CRUD asadmin commands implementation in GlassFish Introduction Many administrative commands in GlassFish are basically simply creating, modifying (rarely) or deleting configuration XML snippets. Such code is quite repetitive to write and error prone. Therefore GlassFish has a feature to support CRUD commands that can create, delete and list XML elements from a CLI invocation without requiring to write code. General solution Notion of parent. Every configuration element has a parent in the XML configuration tree. The parent can reference its children through accessor methods of two basic types :
- List<ChildType> getChildren() : used when there can be 0..n children of a single type associated with one instance of the parent.
- void setChild(ChildType child) : used when there can be 0 or 1 child associated with one instance of the parent.
As a reminder. there cannot be a set of children of the same types associated to their parents through two distinctive accessor methods. Create method skeleton code For the most basic commands, all the administrative commands to create elements are implemented in the following pattern :
- Retrieve the parent instance the child will be added to
- Start a transaction of the parent instance
- Create a child instance
- Set the newly created child instance with the CLI command parameters (initial value of the child attributes).
- Add the child the parent instance through one of the accessor method described above
- Commit the transaction.
As you can see the generic Create command implementation can do most of these tasks provided it's given a few vital pieces of information:
- A way to retrieve the parent type and instance to add the child to give the CLI command parameters
- The child type
- The mapping between a CLI command parameters and the XML attribute of the child
- The accessor method to add the child to the parent.
Delete method skeleton code Even more simple in its implementation than create commands, the delete methods usually follow the pattern :
- Retrieve the instance to be deleted
- Start a transaction on its parent
- Delete the child by removing it from the list or calling setXXX(null)
- Commit the transaction
Therefore the generic Delete command implementation can do most of its tasks provided the following :
- A way to retrieve the child instance give its type and CLI command parameters
- The accessor method to remove the child.
Resolver As described above, there is a need to retrieve instances of a particular type to be able to perform the command. Such responsibility is handled by the CrudResolver implementation, this is its interface :
package org.glassfish.config.support;
/**
* A config resolver is responsible for finding the target object of a specified
* type on which a creation command invocation will be processed.
*
* Implementation of these interfaces can be injected with the command invocation
* parameters in order to determine which object should be returned
*
* @author Jerome Dochez
*/
@Contract
public interface CrudResolver {
/**
* Retrieves the existing configuration object a command invocation is intented to mutate.
*
* @param context the command invocation context
* @param type the type of the expected instance
* @return the instance or null if not found
*/
<T extends ConfigBeanProxy> T resolve(AdminCommandContext context, Class<T> type);
}
Given a admin command execution context, plus the ability to get injected with the CLI command line parameters (or any other HK2 services if necessary), the resolver should be able to determine the particular instance to mutate. Take an example of create-application-ref command, the resolver will be responsible for returning an instance of Cluster or Server using the --target CLI parameter. Most of the Resolver have already been written and although it is possible we need more, you should look first below it one of the stock resolver fits your needs :
- org.glassfish.config.support.TargetBasedResolver : Use the --target CLI parameter and the expected return type to look up the instance
- org.glassfish.config.support.TargetAndNameBasedResolver : On top of using --target to identify a <config> element, it also uses a name to look for a <config> subelement of the requested type and name.
- org.glassfish.config.support.TypeAndNameResolver : Use the requested type and CLI parameter name to find the instance. This is useful for configuration that have the @Index annotation which is used to register such instances under a name in the habitat.
Annotations @Create By placing the @Create annotation on a method, you give the system the following information :
- the method's class is the TYPE of the parent
- the return type of the method indicates the TYPE of the child
- the annotated method is the accessor method to add children of that type to the parent.
All you know need is a resolver. Let's take an example :
/**
* Clusters configuration. Maintain a list of {@link Cluster}
* active configurations.
*/
@Configured
public interface Clusters extends ConfigBeanProxy, Injectable {
/**
* Return the list of clusters currently configured
*
* @return list of {@link Cluster }
*/
@Element
@Create(value="create-cluster")
public List<Cluster> getCluster();
}
In this example the default resolver which just use the parent type (here Clusters) will be used to fetch the unique instance from the habitat. The name is called "create-cluser" and that's what the use will need to type on the CLI invocation. @Delete The delete annotation follows the same design principle as create, by placing an @Delete on a method, you specify :
- the method's class is the TYPE of the parent the child will be removed from
- the return type (or method parameter depending on accessor type) indicates the TYPE of the child
- the annotated method is the accessor to mutate the parent.
Again, all you need on top of this is the resolver. let's build on the previous example :
/**
* Clusters configuration. Maintain a list of {@link Cluster}
* active configurations.
*/
@Configured
public interface Clusters extends ConfigBeanProxy, Injectable {
/**
* Return the list of clusters currently configured
*
* @return list of {@link Cluster }
*/
@Element
@Create(value="create-cluster")
@Delete(value="delete-cluster", resolver= TypeAndNameResolver.class)
@Listing(value="list-clusters")
public List<Cluster> getCluster();
}
The resolver is now the type and name since it will use the expected child type and passed name parameter to fetch the cluster instance from the habitat. The resolver will end up doing habitat.getComponent(Cluster.class, name) where name is the CLI --name parameter value. @Listing Annotate with @Listing to get a listing command. The annotated method should always return a List<ChildType> type and resolver is responsible for finding the instance on which the accessor method will be invoked. The list command supports the following features:
- The default (short) output consists of just the value of the key field for the type.
- The long output (indicated with the --long or -l option) consists of each of the attributes of the type plus any additional duck typed methods that are annoated with @ListingColumn.
- A --header (or -h) option can be used to suppress the header that is part of the long output. This option is ignored if --long is not specified.
- The --output (or -o) option can be used to specify which columns to include in the output. If --output is used, the --long output is automatically selected.
- The @ListingColumn annotation can be used to
- exclude attributes from the output
- specify an alternate heading for that attribute (the default is the uppercase form of the XML name for the attribute)
- specify an ordering of the columns
- specify whether the column should be included in the default output for the --long option.
When used with a DuckTyped method, the ListingColumn annotation must be used with method that returns a String or something that can be converted to a String. The method name must start with "get" and take no arguments, as in:
@DuckTyped
@ListingColumn
String getCalculatedVersion();
Taking back the previous example, we can now have :
@Configured
public interface Clusters extends ConfigBeanProxy, Injectable {
/**
* Return the list of clusters currently configured
*
* @return list of {@link Cluster }
*/
@Element
@Create(value="create-cluster", decorator=Cluster.Decorator.class)
@Delete(value="delete-cluster", resolver= TypeAndNameResolver.class, decorator=Cluster.DeleteDecorator.class)
@Listing(value="list-clusters")
public List<Cluster> getCluster();
}
Decorators Sometimes, CRUD commands are not enough, more xml editing needs to happen during the command invocation. Examples of decorators The following examples depicts typical cases of a decorator need.
- create-cluster command need to not only create the <cluster> XML snippet under <clusters> but it should also add a <config> element under <configs> when there are no config-ref parameter passed on the command line. The CRUD command implementation will take care of creating the <cluster> element, the decorator will have to create the <config> element if necessary.
- create-cluster command also needs to add sub-element to the <cluster> XML snippet. These sub elements like application-ref, resource-ref and similar are immediate children of <cluster> and do not necessary need any parameters values from the CLI invocation.
- delete-cluster command need to delete the referenced <config> element if the deleted cluster element was the last one referencing it. The generic delete command will take care of deleting the <cluster> element, the decorator will optionally delete the <config> element.
CreationDecorator A creation decorator is called during the @Create generic create command implementation. The interface is :
@Scoped(PerLookup.class)
public interface CreationDecorator<T extends ConfigBeanProxy> {
/**
* The element instance has been created and added to the parent, it can be
* customized. This method is called within a {@link org.jvnet.hk2.config.Transaction}
* and instance is therefore a writeable view on the configuration component.
*
* @param context administration command context
* @param instance newly created configuration element
* @throws TransactionFailure if the transaction should be rollbacked
* @throws PropertyVetoException if one of the listener of <T> is throwing a veto exception
*/
public void decorate(AdminCommandContext context, T instance) throws TransactionFailure, PropertyVetoException;
/**
* Default implementation of a decorator that does nothing.
*/
@Service
public class NoDecoration implements CreationDecorator<ConfigBeanProxy> {
@Override
public void decorate(AdminCommandContext context, ConfigBeanProxy instance) throws TransactionFailure, PropertyVetoException {
// do nothing
}
}
}
It is VERY important to realize the instance passed is the writable copy of the newly added element, which mean you do not need to start a new transaction to change this instance, calling setXXX on it will succeed as the decorator method is called as part of the element creation transaction. Another side effect is that there is only one transaction for all possible changes of a CRUD command making it easy to rollback in case of conflicting changes. Continuing on our cluster example, the @Create declare its decorator :
@Configured
public interface Clusters extends ConfigBeanProxy, Injectable {
/**
* Return the list of clusters currently configured
*
* @return list of {@link Cluster }
*/
@Element
@Create(value="create-cluster", decorator=Cluster.Decorator.class)
public List<Cluster> getCluster();
}
and the decorator implementation is in the Cluster definition :
@Configured
public interface Cluster extends ConfigBeanProxy, ... {
....
@Service
@Scoped(PerLookup.class)
class Decorator implements CreationDecorator<Cluster> {
@Param(name="config", optional=true)
String configRef=null;
@Inject
Domain domain;
@Override
public void decorate(AdminCommandContext context, final Cluster instance) throws TransactionFailure, PropertyVetoException {
...
}
}
}
Things to notice :
- we use an inner class, you don't have to.
- the decorator can be injected with the CLI command parameters
- the decorator can be injected with any HK2 services or configuration instances.
DeletionDecorator Similar ruling applies for the deletion decorator, except the interface is slightly different :
/**
* A decorator for acting upon a configuration element deletion.
*
* @param <T> the deleted element parent type
* @param <U> the deleted element
*
* @author Jerome Dochez
*/
@Scoped(PerLookup.class)
public interface DeletionDecorator<T extends ConfigBeanProxy, U extends ConfigBeanProxy> {
/**
* notification of a configuration element of type U deletion.
*
* Note that this notification is called within the boundaries of the
* configuration transaction, therefore the parent instance is a
* writable copy and further changes to the parent can be made without
* enrolling it inside a transaction.
*
* @param context the command context to lead to the element deletion
* @param parent the parent instance the element was removed from
* @param child the deleted instance
*/
public void decorate(AdminCommandContext context, T parent, U child);
}
In this case, the parent is a writable copy which can be modified without further configuration transaction enrollment. The child reference is however read only, since it's being deleted, there is no need to modify it.
@Configured
public interface Cluster extends ConfigBeanProxy, ... {
..
@Service
@Scoped(PerLookup.class)
class DeleteDecorator implements DeletionDecorator<Clusters, Cluster> {
....
}
}
Clustering information Just like normal command that can be implemented with real code and annotated with @ExecuteOn, CRUD commands execution can have clustering information added to their configuration. A @ExecuteOn annotation can be added to the @Create or @Delete annotations to specify the cluster specific meta-data. For instance, the following declaration will make the create-instance command only run on the DAS and not on server instances.
@Create(value="create-instance", resolver= TypeResolver.class, decorator= Server.CreateDecorator.class,
cluster=@org.glassfish.api.admin.ExecuteOn(value = RuntimeType.DAS))
By default, there is an implicit @ExecuteOn() annotation to CRUD commands, which mean that will CRUD command will use the default @ExecuteOn annotation values to specify their clustered behaviours. Reference @Create
public @interface Create {
/**
* Name of the command that will be used to register this generic command implementation under.
*
* @return the command name as the user types it.
*/
@Index
String value();
/**
* Returns the instance of the parent that should be used to add the newly created
* instance under. The implementation of that interface can use the command parameters
* to make a determination about which instance should be used.
*
* @return the parent instance.
*/
Class<? extends CrudResolver> resolver() default CrudResolver.DefaultResolver.class;
/**
* Returns a decorator type that should be looked up and called when a new
* configuration element of the annotated type is created.
*
* @return a decorator for the annotated type
*/
Class<? extends CreationDecorator> decorator() default CreationDecorator.NoDecoration.class;
/**
* Returns the desired behaviors in a clustered environment. By default, using all the
* {@link Cluster} default values
*
* @return the cluster information
*/
Cluster cluster() default @Cluster();
}
@Delete
public @interface Delete {
/**
* Name of the command that will be used to register this generic command implementation under.
*
* @return the command name as the user types it.
*/
@Index
String value();
/**
* Returns the instance of the configured object that should be deleted.
* The implementation of that interface can use the command parameters
* to make a determination about which instance should be used.
*
* @return the instance targeted for deletion.
*/
Class<? extends CrudResolver> resolver() default CrudResolver.DefaultResolver.class;
/**
* Returns a decorator type that should be looked up and called when a
* configuration element of the annotated type is deleted.
*
* @return a deletion decorator for the annotated type
*/
Class<? extends DeletionDecorator> decorator() default DeletionDecorator.NoDecoration.class;
/**
* Returns the desired behaviors in a clustered environment. By default, using all the
* {@link org.glassfish.api.admin.Cluster} default values
*
* @return the cluster information
*/
Cluster cluster() default @Cluster();
}
@Listing
public @interface Listing {
/**
* Name of the command that will be used to register this generic command implementation under.
*
* @return the command name as the user types it.
*/
@Index
String value();
/**
* Returns the instance of the parent that should be used get the list of children.
*
* @return the parent instance.
*/
Class<? extends CrudResolver> resolver() default CrudResolver.DefaultResolver.class;
}
Multiple CRUD commands It is possible to have several CRUD commands defined on a single method declaration, this will allow to specify a different resolver or more importantly a different decorator or cluster annotation to be used between the 2 commands. To declare multiple create or delete commands on a single method, use the @Creates or @Deletes annotation.
@Creates(
@Create(value="create-something", decorator=CreateSomething.Decorator)
@Create(value="create-something-else", decorator=CreateSomethingElse.Decorator)
List<Something> getSomethings();
)
Since the resulting element of these 2 CRUD commands will be the same type, only the decorator and resolvers can vary the list of parameters each command take. All the @Param annotated attributes of the targeted type (Something in the example above) will be parameters of both commands.
|