Home About Download Tutorial Papers About the author Documentation Home About Download Tutorial Papers About the author

Second case: using domain annotations with frameworks that do not support them

The second functionality provided by Daileon allows domain annotations to be used with frameworks that do not support them. A domain annotation is a custom annotation that is defined when the application is being developed and that represents other annotations. For instance, a domain annotation @Administrative may represent 2 annotations of the EJB 3 API. However, if the domain annotation is used directly in the code, it won't have any effect in the JEE container, even if it represents annotations known by the container. Naturally, in this case, the domain annotations require a special treatment, which can be applied with Daileon.

In order to work, the domain annotations have to be translated to their corresponding annotations before the application is run. There are a few steps that have to be taken in order to use domain annotations with frameworks that do not support them. Let's start with the following example: creating a very simple application whose domain annotations represent annotations of the EJB 3 API.

1st step: defining the application's API.

The first step is to define the application's API, shown in Listing 1. The application consists of a repository that deals with a domain entity called Paper. In the end, this repository will consist of a Stateless Session Bean.

Listing 1. The application's API.
 public interface RepositoryLocal {

     public void create(Paper paper);
     public void update(Paper paper);
     public void delete(Paper paper);
     public Paper getPaper(String paperName);
 }

2nd step: defining the domain annotations.

Suppose that we want to separate the methods in 2 categories: administrative and free query. The administrative methods are create(Paper), update(Paper) and delete(Paper), and the getPaper(Paper) method represents a free query. Therefore, 4 domain anntoations can be created. One to annotate the API, another one to annotate the implementation class, and the annotations for the 2 categories.

Let's suppose that the administrative methods can only be called by callers that are in the role "admin", and also require a transaction. The free query method allows all users to use, no matter the role, and do not require a transaction. The API domain annotation indicates that the repository will only be available to the local JVM, and the domain annotation of the implementation will indicate that the repository will be a stateless session bean.

Often, it is not possible to annotate an annotation with another annotation, since it can only annotate specific elements, such as classes, fields or methods. Therefore, the solution is to create separate classes to keep the annotations that are represented by the domain annotation, and indicate these classes in the domain annotation. Listing 2 contains the code that represents the 4 domain annotations of the example. Each domain annotation is designed specifically for the element type it will annotate, and each domain annotation is annotated with @DomainAnnotation and a correspoding template annotation, both annotations provided by Daileon.

Listing 2. Domain annotations' definition.

 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @DomainAnnotation
 @ClassTemplate(annotatedClass = "net.sf.daileon.app.domain.RepositoryDefinitionAnnotationsHome")
 public @interface RepositoryDefinition {}

 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.TYPE)
 @DomainAnnotation
 @ClassTemplate(annotatedClass = "net.sf.daileon.app.domain.RepositoryAnnotationsHome")
 public @interface Repository {}

 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 @DomainAnnotation
 @MethodTemplate(annotatedClass = "net.sf.daileon.app.domain.AnnotationsHome",
                 method =  "administrative")
 public @interface Administrative {}

 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.METHOD)
 @DomainAnnotation
 @MethodTemplate(annotatedClass = "net.sf.daileon.app.domain.AnnotationsHome",
                 method = "freeQuery")
 public @interface FreeQuery {}

3rd step: annotating the classes.

Now that the domain annotaions are defined, we can annotate the API and the class. Listing 3 contains the code of the domain classes, annotated with the domain annotations.

Listing 3. Domain annotations' definition.

 @RepositoryDefinition
 public interface PaperRepositoryLocal {

     public void create(Paper paper);
     pubic void update(Paper paper);
     public void deletePaper(Paper paper);
     public Paper getPaper(String paperName);
 }

 @Repository
 public class PaperRepository implements PaperRepositoryLocal {

     @DataManagement
     public void create(Paper paper) {
         System.out.println("Adding new paper...");
     }

     @DataManagement
     public void update(Paper paper) {
         System.out.println("Updating paper...");
     }

     @DataManagement
     public void delete(Paper paper) {
         System.out.println("Deleting paper...");
     }

     @FreeQuery
     public Paper getPaper(String paperName) {
         System.out.println("Getting paper by name: " + paperName);
         return null;
     }
 }

4th step: building the classes that keep the annotations that correspond to each domain annotation.

With everything set, we can now build the classes that keep the annotations. These are the same classes indicated in each domain annotation. Looking at Listing 2, we have to build 3 classes: RepositoryDefinitionAnnotationsHome, RepositoryAnnotationsHome and AnnotationsHome. The RepositoryDefinitionAnnotationsHome and the RepositoryAnnotationsHome classes will contain annotations only at class level, and thus won't have any fields or methods. The @Administrative and the @FreeQuery indicate 2 methods at Listing 2, and thus the AnnotationsHome class will have 2 methods.

Listing 4. The classes that keep the annotations.

 @Local
 public class RepositoryDefinitionAnnotationsHome {}

 @Stateless
 public class RepositoryAnnotationsHome {}

 public class AnnotationsHome {

     @TransactionAttribute(TransactionAttributeType.REQUIRED)
     @RolesAllowed("admin")
     public void administrative() {}

     @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
     @PermitAll
     public void freeQuery() {}
 }

5th step: defining the XML file before running Daileon.

Now, it is time to define an XML file that contains all information required by Daileon, so it can run properly. This XML file is pretty simple, and contains the name of all classes that have domain annotations that have to be translated so the application can run properly.

Listing 5. The XML configuration file.
<?xml version="1.0" encoding="UTF-8"?>
<daileon-config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="daileon-config.xsd">
    <setup>
        <root-directory>C:\Domain Application\classes\net</root-directory>
        <destiny-directory>C:\Domain Application\modified classes</destiny-directory>
        <strategy-class>net.sf.daileon.notsupported.AnnotationsStrategy</strategy-class>
        <domain-annotations-replacer>net.sf.daileon.notsupported.DomainAnnotationsKeeper</domain-annotations-replacer>
    </setup>
    <domain-annotations>
        <annotated-class>net.sf.daileon.app.domain.PaperRepositoryLocal</annotated-class>
        <annotated-class>net.sf.daileon.app.domain.PaperRepository</annotated-class>
    </domain-annotations>
</daileon-config>

The XML file, shown in Listing 5, contains all information necessary so that the annotations can be translated correctly to their corresponding annotations. First, the root-directory element indicates the root directory of the classes that cointain domain annotations. For instance, in this example, the physical path of the PaperRepository class is C:\Domain Annotations\classes\net\sf\daileon\app\domain\PaperRepository, therefore, the root directory is C:\Domain Annotations\classes\net. The destiny-directory element indicates where the classes will be copied to. The classes are first copied to this location and then the domain annotations are translated. This is means that the domain annotations are kept in the original code, so that they can be translated to other annotations, if necessary. Note that the framework translates domain annotations in the bytecodes, so the root-directory element indicates the root directory of the compiled classes.

There are 2 aspects that the developer may customize when using the framework: how the annotations are organized and if the domain annotations should be kept in the code. For the first aspect, the framework provides a default implementation (called AnnotationsStrategy), which assumes that the annotations that correspond to each domain annotation is present in template classes indicated by each domain annotation. However, the developer may organize the annotation in XML files, or properties files, or even databases. To do so, the developer has to create a class that implements the net.sf.daileon.notsupported.Strategy interface and indicate it in the XML configuration file, in the strategy-class element. If this element is not indicated, then the framework uses the default implementation.

For the second aspect, there are two implementations that the developer can use: DomainAnnotationsKeeper and DomainAnnotationsDiscarder. Each implementation either keeps or discards all domain annotations in the translated code when translating each of them. However, the developer may decide to keep or discard them in particular situations. To do so, the developer has to create a class that extends the AbstractAnnotationsReplacer class and implement the keepDomainAnnotations() method. This class (or a default implementation) has to be indicated in the domain-annotations-replacer element of the XML configuratio file. If this element is not indicated, then the framework uses the DomainAnnotationsKeeper implementation.

And finally, each class that contains a domain annotation is indicated in the annotated-class elements. The framework will only act in the classes indicated in these elements.

6th step: performing the transformations.

With everything set, the only thing left to do is to run the framework. The physical path of the XML file has to be indicated when running it. This can be done in the following way:

java -cp C:\daileon-1.0.jar -jar daileon-1.0.jar C:\daileon-config.xml

If everything is correct, the classes containing the final annotations will be copied to the location indicated in the XML configuration file. After the transformation, the classes of the example will have the following format:

Listing 6. The classes of the example after the transformation.

 @Local
 public interface PaperRepositoryLocal {

     public void create(Paper paper);
     pubic void update(Paper paper);
     public void deletePaper(Paper paper);
     public Paper getPaper(String paperName);
 }

 @Stateless
 public class PaperRepository implements PaperRepositoryLocal {

     @TransactionAttribute(TransactionAttributeType.REQUIRED)
     @RolesAllowed("admin")
     public void create(Paper paper) {
         System.out.println("Adding new paper...");
     }

     @TransactionAttribute(TransactionAttributeType.REQUIRED)
     @RolesAllowed("admin")
     public void update(Paper paper) {
         System.out.println("Updating paper...");
     }

     @TransactionAttribute(TransactionAttributeType.REQUIRED)
     @RolesAllowed("admin")
     public void delete(Paper paper) {
         System.out.println("Deleting paper...");
     }

     @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
     @PermitAll
     public Paper getPaper(String paperName) {
         System.out.println("Getting paper by name: " + paperName);
         return null;
     }
 }

If necessary, the domain annotations can be translated again to other annotations. This promotes reusability and modularity. The same code may be reused with other frameworks, and at the same time, the domain code is free of interests other than the domain itself.