org.openide.loaders
handles the association of files together into groups and assigning
types to data.
org.openide.cookies
provides a design pattern for attachable behaviors for data objects and nodes.
org.openide.util.datatransfer
implements some extensions to the clipboard.
myform.java myform.form myform.class myform$1.class myform$2.classAll of these files are handled by the IDE as a single data object with a single set of actions applicable to it. In the Explorer, only one master node is created for this data object (though it has some substructure)--the user does not see these files as isolated.
DataShadow
s,
are interpreted using DataSystems, not FileSystems.
MultiFileLoader
.
Like the loader used by the Form Editor, they are able to recognize
multiple files at once and create a data object from them. All data
objects have a
primary file
which is representative of the data object;
e.g. myform.java
in the previous example. As well, these
multi loaders may have any number of
secondary files.
The basic mechanism a multi-file loader uses, is that the loader
pool will pass it file objects to recognize in an arbitrary order; the
loader may get a primary or secondary file first. Either way, it must
recognize that it belongs to a cluster; find the primary file if it
got a secondary one; and create a new
MultiDataObject
containing the supplied file as an
entry.
When other files in the cluster are passed to the loader, it must
create new entries in the same multi data object, to indicate
their association.
Single-file loaders
These typically subclass
UniFileLoader
.
A single-file loader is of course simpler, and is likely to be more
commonly used (since the majority of file types make sense by
themselves). The default implementation makes it straightforward to
create a subclass recognizing only certain file extensions.
This kind of loader may be used for, e.g., HTML files which ought to be recognized as such and given a simple data object appropriate to working with HTML (so that opening it by default launches a web browser, and so on).
Note that the standard UniFileLoader
is actually a
special type of MultiFileLoader
(only recognizing one
file), so that it actually creates a
MultiDataObject
when it recognizes its file. Normally you will use a
UniFileLoader
for most purposes, so the behaviors in
MultiDataObject
are generally available as defaults. If
you had some reason to avoid using this kind of data object, you could
of course subclass DataLoader
and DataObject
directly.
Entries
Entries represent a single file in a MultiDataObject
, and
thus are commonly used in all loaders (even single-file
loaders). Normally
MultiDataObject.Entry
will not be specially subclassed; most module authors will use
FileEntry
(a regular entry) and sometimes
FileEntry.Numb
(a discardable file which should not be slavishly moved about just
because the primary file is).
Entries primarily handle operations on the whole file, such as
copying or instantiation from template, and provide the ability to
insert special behavior hooks into such operations, if desired. For
example, a Java source entry might want to rename its package and
class name when it was moved. (The easiest way to create "smart"
templates such as this is to make the entry a
First of all, you must be able to register your loader in the
module so that the IDE knows about it. To do so, you may use the
Modules API to add a special
loader section
to your module.
Pay attention to whether this loader is potentially in conflict
with other existing or probable loaders; if so, you should specify
that it take higher or lower precedence than these others. (This would
be necessary, e.g., if your loader recognized everything that another
loader did, and used that loader, but also added further special
behavior.)
FileEntry.Format
.)
Getting a data object
Normally you do not need to explicitly retrieve a data object--the
loader pool will create them on demand, present them to the user in
the Explorer as nodes, and user actions will trigger use of the data
object. However, if you do need to retrieve the data object for a file
object you may do so using
DataObject.find(...)
,
which will create a new data object for the file if it needs to,
otherwise will return the existing one. (Catch
DataObjectNotFoundException
in case the file is not recognizable to any loader.)
Node Delegates
Data objects have separate, but associated,
Node
s
which represent them in the Explorer and present a meaningful
interface to the user. You may use
DataObject.getNodeDelegate()
to retrieve the node delegate for a data object, if necessary.
Writing a loader and data object
This section is the most important for most people: after a loader has
been correctly written and installed, the system takes care of
providing files for it to recognize; constructing nodes for the files;
etc.; and most further implementation of a module will be callbacks
run in response to some aspect of the objects created here.
Module installation
Deciding what to subclass
Your first step in writing the loader is deciding what to
subclass. Though you could in principle subclass
DataLoader
directly, in practice it is easier and better
to subclass either
MultiFileLoader
or
UniFileLoader
,
according to whether your loader needs to cluster files, or just needs
to deal with a single primary file.
UniFileLoader.findPrimaryFile(...)
to return its argument if this file is of the type that should be
handled by your loader, or null
if it is not.
In fact, providing that your loader is of the common sort that just
looks for a specific file extension, you do not even need to override
this method at all; simply create an appropriate
ExtensionList
and call
UniFileLoader.setExtensions(...)
in your loader's constructor.
For a multi-file loader, the situation is slightly more complex,
but still the IDE takes care of most of the work for you. You should
implement
MultiFileLoader.findPrimaryFile(...)
as follows:
FileUtil.findBrother(...)
,
or perform a similar lookup by hand.
null
.
MultiFileLoader.createMultiObject(...)
in order to do this. The method will be passed the correct primary
file object for you to work with.
To write the data object, subclass
MultiDataObject
.
Your constructor will call the superclass constructor (so the loader
will pass in the desired primary object and a reference to
itself). You do not need to worry about whether or not to
throw
DataObjectExistsException
in the constructor; it will be thrown automatically by the superclass if necessary.
After that, what to override in the data object is up to you. Other
than things mentioned below, you may find it useful to prevent the
data object from being renamed or otherwise tampered with, if doing so
would make it useless or corrupted in some way; just return
false
from e.g.
MultiDataObject.isRenameAllowed()
.
Or, if e.g. moves are to be permitted but require special
treatment, you may override e.g.
MultiDataObject.handleMove(...)
.
UniFileLoader.createPrimaryEntry(...)
just produces a
FileEntry
,
which is most likely what you want.
For multi-file loaders, you must explicitly select the entry types
by implementing
MultiFileLoader.createPrimaryEntry(...)
and
MultiFileLoader.createSecondaryEntry(...)
.
Typically, the primary entry will be a FileEntry
, and
will behave normally. The secondary entry might also be a
FileEntry
, if it makes sense to move the secondary
entries along with the primary (i.e., if they are valuable enough to
do so, and will not be corrupted); in many cases you will want to use
a
FileEntry.Numb
,
which will not be moved along with the primary file, and may just
be discarded (for example, this would be useful for compiled
*.class
files, cached indices, etc.). For such dummy
files, you will generally also want to use
FileObject.setImportant(...)
to prevent the file from being considered by a version control
system, for example.
It is possible to specify custom copy/move/etc. behavior for
individual files in your data object by subclassing
MultiDataObject.Entry
(or FileEntry
)
and providing a non-obvious implementation. If you need custom
behavior for the whole data object at once, it is preferable to do so
by overriding methods on the data object, as mentioned above.
FileEntry.Format
is a convenient entry type to use if you wish to perform substitution of some type
of token when creating the file from template. Typically the method
FileEntry.Format.createFormat(...)
will be implemented to return an instance of
MapFormat
containing substitution keys and values according to the name and package of the file object;
constants used by the module; values of associated system options; the current time and
date or user name; etc. For example, the Java data loader uses this entry type with a customized
MapFormat
permitting it to replace keys such as __NAME__
with the
(new) name of the class, or __USER__
with the current user name (as taken from a
system option, defaulted from the Java system property).
SharedClassObject
,
which means that there is only intended to be a single instance of the loader
per class, and all associated configuration and properties are stored in a shared
state pool. SharedClassObject
manages this state implicitly; if you
wish to associate any properties with a data loader, you should:
SharedClassObject.getProperty(key)
and
SharedClassObject.putProperty(key, value, true)
(the latter will automatically fire property changes and synchronize for you).
readExternal
and writeExternal
to read and write
your property values from the stream. Please always first call the super methods.
You should use the method
SharedClassObject.initialize()
to set up the shared instance, including both your own properties, and standard ones such as
DataLoader.setDisplayName(String)
,
DataLoader.setActions(SystemAction[])
,
and
UniFileLoader.setExtensions(ExtensionList)
.
Finally, data loaders will be customized by the user as Beans (and persisted using externalization). For this reason, they should have an associated bean info class which should typically specify:
One simple way to add cookie support to a data object is simply to implement the cookie's interface on the data object itself; then it will automatically support the cookie. But doing so for many cookies may be a bad idea, as your data object class will become cluttered; and there is no way to alter the set of cookies provided in this way.
A better technique is to provide cookies explicitly from
DataObject.getCookie(...)
.
Assuming that you are subclassing MultiDataObject
, you
need not override this method yourself, but rather should call
MultiDataObject.setCookieSet(...)
in the constructor, creating a new cookie set with the cookies you
want to provide by default. Then it is possible to extend this set
later, and to more easily examine its contents.
You may attach some
actions
to the nodes associated with your data
objects. The easiest way to do this is to call
DataLoader.setActions(...)
in your loader's
SharedClassObject.initialize()
method, which lets you provide a set of
actions appropriate to all data objects created by this
loader. Please see the Actions API for
an example displaying suggested standard actions to include, and
their positioning.
Or, you may wish to selectively attach actions to certain data
objects' nodes and not others. If you need to do this, please override
DataNode.getActions()
when creating your node delegate; you probably want to call the super
method and append any additional actions this particular node should
have.
The nodes ought also to have a default action, which will be
performed in response to a generic user-initiated event, such as a
double-click on the node; this should do something safe and obvious on
the node, such as opening it for editing, running it if executable,
etc. To do so, your node delegate should override
DataNode.getDefaultAction()
.
If unspecified, the IDE may still provide a generic default action,
such as displaying properties of the object.
The default implementation is only specified in the case of
templates, so you may override this. However, if there is a chance
this data object might serve as a template, for UI consistency this
default action should be preserved; you may check
DataObject.isTemplate()
,
and if true, provide
InstantiateAction
as the result.
Creating a node delegate
You must create a
Node
to represent your data object in the Explorer hierarchy, so that the
user may interact with it visually. The method
DataObject.createNodeDelegate()
controls what sort of node should be created. To control the icon, you
should use
AbstractNode.setIconBase(...)
on the newly created DataNode
, either in its constructor
(if subclassing) or in DataObject.createNodeDelegate()
.
You have considerable latitude in creating this node; e.g. the Form Editor actually creates a full hierarchy for nodes representing forms, including one subtree representing the Java class object (and its various members), as well as a subtree representing the component structure of the form (as displayed in the Component Inspector).
For simple loaders, it is not typically necessary to create a special node subclass for the delegate, as you may provide an icon, cookies, and common actions without doing so.
For data objects which ought to have a different representation
in the Object Browser (or others users of
DataObjectFilter
)
than in the Explorer, you should
provide
an implementation of
ElementCookie
,
whose alternate node may also usefully provide
filtering support.
In this case, the normal node delegate is not used.
System loaders
The IDE installs a few extra "hidden" loaders into the loader pool, as
are returned by
DataLoaderPool.allLoaders()
.
Currently, these include:
DataFolder
for them. This folder will have a few standard system actions
associated with its
node,
can be sorted, etc.
DataShadow
,
and generally behaves just like the object it points to.
Overriding a system loader is not very useful if the file might
have already been recognized by a system loader, which could be the
case if it was scanned for a data object before the custom loader got
to it (and this is difficult to predict); so this would better be done
upon
creation
of the file object by the underlying filesystem (which would need to
cooperate with the loader in this regard).
Cookies
Cookies provide a way for both data objects and nodes to indicate to
the IDE in a flexible fashion what types of (usually user-initiated)
operations they are capable of supporting, and even to dynamically add
and remove these capabilities.
What is a cookie?
A cookie is a design pattern used to separate the
presentation of implementation of some interface from the actual
object that implementation ought to be associated with. As such, it is
a convenient way of removing the requirement that all interfaces
presented by the cookie holder (either a data object or
node)
be actually implemented by the primary Java class of the object. If
desired, the primary Java class can in fact be the cookie implementor,
though this is a special case--some holder objects just declare
themselves by default to hold all cookies their own class implements.
Take care not to confuse this NetBeans usage of the word with a different meaning sometimes used in computer science, that of an opaque callback object.
For purposes of marking a cookie as such, the cookie interface
should extend
Node.Cookie
.
Other than that, there are no requirements as to what makes a valid
cookie--typically it will provide a small set of abstract operations,
such as "opening", "compiling", "searching", etc.
Uses of cookies on nodes are not much different from uses on data
objects; the initial cookie detection is done by
In short, there are a number of ways to attach cookies to either a
node or data object (and you may listen for changes in the set of
supported cookies, etc.). In all cases, the
Using
cookie sets,
it is possible to dynamically add and remove cookies from a
holder. This is appropriate for cookie types whose applicability may
vary over time. For example, some objects may be compilable at certain
times, but if they have been compiled recently and are already
up-to-date, this support should be temporarily disabled. In the common
case that an action (say, in the system Build menu for Compile) is
sensitive to the cookie provided by the currently selected object (in
this case the compilation cookie), this menu item can be automatically
greyed out when necessary; and then reenabled when either the
selection changes, or the object changes state so as to make the
cookie appropriate once more, at which time the folder readds the
cookie to its cookie set and
fires a property change
to this effect.
While it is possible for any data object or node to support any
given cookie ad-hoc if it has a special way of doing so, more commonly
there is a standard way (or standard set of ways) that a particular
cookie is likely to be implemented, and the primary job of the holders
is just to declare that they want to use it. In this case, the
standard implementing class is called a support, and
typically its constructor will provide an association to the holder in
some fashion, and the support itself will determine what to do with
the holder object when invoked.
For example, any holder could provide an implementation of
Sometimes a cookie support that is applicable to multiple cookies
may not implement any of them, leaving the choice of which to declare
implementation of, to a subclass. This is the case with the abstract
Often, in conjunction with writing a cookie you may want to create
an action which is sensitive to that
cookie. Then this action may be installed globally in the system (for
example, in a menu bar, on a shortcut, etc.), and only activated when
the current selection provides the cookie.
If the support is designed to be usable from someone else's loader,
and it is not obvious for which data objects using the support is
possible, you may be well advised to include a public, static tester
method in the support class indicating whether it would function
correctly with a given file entry/data object. This way, an
independently written loader could easily add the cookie with your
support to any data object it had, without knowing the details of its
prerequisites.
Enabling complex customizations on both sides of a cut/copy-paste
can be confusing; the
Nodes API
contains a detailed description of these operations as they pertain to
nodes, which may be helpful.
To write a convertor, just implement
The data associated with Node.getCookie(...)
;
however the default data node uses
DataNode.getCookie(...)
to both look in the node's specific
cookie set,
and to look through the underlying data object's
cookies
(which may also be provided via a
MultiDataObject.getCookieSet()
).
Attaching and retrieving cookies
getCookie(Class)
method is used by the system to
determine whether or not a given cookie is supported: thus, cookies
are identified by their representation class, i.e. the Java
class of the cookie interface. The system will expect to find a cookie
object assignable to that representation class, and this object will
have methods invoked on it to perform the proper action; it is the
responsibility of that cookie object to associate itself to whatever
node or data object is holding it.
Supports
ExecCookie
that would somehow start execution of something. But normally in the
IDE this is used for class files with an appropriate main
method, which will be invoked as from the Java launcher. So, the
standard support
ExecSupport
performs this scanning for the main method, construction of the proper
execution engine, and so on. It also happens to implement
ArgumentsCookie
and
DebuggerCookie
,
since these cookies are also appropriate under the same
conditions. So, whatever data loader is responsible for creating data
objects corresponding to compiled class files makes sure that any
class files with a main
have this support associated with
them, passing in the relevant file entry in the
constructor.
The node delegates for these classes will also implicitly contain the
same cookies, of course, so the user is able to invoke the Execute
action on these nodes (disabled on other nodes).
Standard cookies and supports
Most standard cookies you would want to use exist in the
org.openide.cookies
package. Many of these have standard supports as well, frequently in
org.openide.loaders
.
Using the Javadoc, the surest way to find cookies and supports is to
look at
Node.Cookie
and browse the subinterfaces; for any cookie of interest, just look
for implementing classes, which are usually supports. (A few cookie
implementations are not of general utility, and so are not documented
as being supports.) The standard supports generally take a
file entry
in their constructor, as they are designed for use by loaders
attaching them to data objects.
OpenSupport
,
which actually may be used for any or all of
OpenCookie
,
ViewCookie
,
or
CloseCookie
,
according to the needs of its subclass and holders.
EditorSupport
,
for instance, uses only OpenCookie
and
CloseCookie
.
Writing a cookie
Writing a new cookie requires no special knowledge--just extend
Node.Cookie
and add interface methods according to whatever you need done.
Writing a support for an existing cookie
Writing a support also does not require anything unapparent. It should
be a concrete class, its name conventionally ending in
Support
, implementing the cookie interface. Normally it
should have one constructor, taking a
MultiDataObject.Entry
as principal argument, so as to encourage its use in the context of a
loader; the file entry of course gives easy access to the file object
it represents, as well as the data object via
MultiDataObject.Entry.getDataObject()
.
Using an existing support
Using an existing support is generally straightforward--assuming your
data object is a subclass of MultiDataObject
, you may
just add a new instance of the support using
MultiDataObject.setCookieSet(...)
in the constructor, passing in the primary entry (most likely) from
MultiDataObject.getPrimaryEntry()
.
Extended Clipboard
The IDE implements an extended clipboard, which enhances the functions
provided in the
java.awt.datatransfer
package. This implementation is to be found in
org.openide.util.datatransfer
.
Convertors
The extended clipboard,
ExClipboard
,
provides the ability for a prebuilt transferable to support additional
data flavors that it was not originally designed for; then the
convertor supplies the implementation of these conversions.
ExClipboard.Convertor
.
The Javadoc should provide sufficient information on its sole
method. Installing the convertor is easy; you can just add a special
module section for it, as described in the
Modules API.
Event notification
The extended clipboard supports Java Event-based notification of
changes in the contents of the clipboard. Just register your listener
with
ExClipboard.addClipboardListener(...)
.
Multi-transfers
ExTransferable.Multi
is a special transferable type used to transfer multiple objects (not
necessarily of the same type) at once. It only supports one, virtual
data flavor,
ExTransferable.multiFlavor
.
multiFlavor
in the
ExTransferable.Multi
will always be a special container
object,
MultiTransferObject
.
It is designed to permit access to its constituent real
Transferable
s.
UML Diagrams
Data objects
Data loaders
Built on February 22 2001. | Portions Copyright 1997-2000 Sun Microsystems, Inc. All rights reserved.