Monday, April 18, 2011

Annotation, A journey Novice to Ninja


                                                                                                                             

Annotation, A journey Novice to Ninja

Annotation

Annotation is similar to interface which marked with @ symbol. Annotation extends the semantic of the normal class, variables and methods declaration.   Annotations are metadata which provides additional information for the class, instance and class fields/variable and methods.
Now days, majority of framework builds based on Annotation to simplify the coding part as well as configuration part required specific to the framework. If basic concept of Object Orient is good then, it is very easy to switch from any other technology and programming language which is based on Object Oriented principle. Each object oriented language differs in terms of syntax but concept remains the same. Similarly, if we know the basic concept of annotation then learning any framework based on annotation is very easy. We have to learn the purpose of annotation provided by the underlying framework and its applicability nothing else.
Let’s begin with simple annotation used by java to discourage the use of any method and class which is deprecated/obsolete in new release. Best example of discourage the use of class; method, fields can be annotated using @Deprecated annotation.
Code snippet 1       

This code demonstrates the use of Deprecated annotation. Deprecated annotation discourages the use of deprecated /obsolete methods in code.  Compiler generates the appropriate warnings to avoid use of deprecated methods, fields and classes.
/**
 * DemoAnnotaion.java
 * Created on Apr 16, 2011
 */

package com.novice.annotation;

/**
 * @author Ashish.Chudasama
 * @deprecated This is deprecated class. Check NinjaAnnotation class for more details.
 */
@Deprecated
public class DemoAnnotaion
{
    @Deprecated
    int id ;
   
    public void findById(Integer id)
    {
    }
    /**
     *  @deprecated This is deprecated method. Use findById(Integer id).
     */
    @Deprecated
    public void findById(Object id)
    {
    }
}

If you observer carefully @deprecated and @Deprecated annotation used in above code one for documentation purpose whereas second one is to provide intelligence to the compiler to generate the appropriate warnings during compilation time .

Custom annotations

Declaration of custom annotation:


Declare Remark annotation by which developer can put appropriate remarks for each of the program elements.
Code snippet 2

package com.novice.annotation;

public @interface Remark
{
    public String author();
    public String shortDescription();
    public String reviewBy() default "";
}

Key points:
1)      @interface indicates that Remark is custom annotation and not an interface.
2)      If you forget to write @ before the interface keyword then its interface not an annotation.
3)      Default value can be assign within property/method declaration. Default value for the reviewBy() is empty string.
4)      Only primitive type, String, Class, annotation, enumeration are permitted or 1-dimensional arrays are permitted as a Return type.
5)      By default retention policy of a custom annotation is class. (explain later)
Code snippet 3

This code snippet describes how to use the @Remark annotation                          
package com.ninja.annotationhelper;

import com.novice.annotation.Remark;

// Apply at class level
@Remark(author="Ashish", reviewBy="TL", shortDescription="Remark for class.")
public class HelperTemplate
{     
// Apply at variable level
    @Remark(author="Ashish", reviewBy="TL", shortDescription="Remark for variable.")
    int variable;
   
//apply at method level
    @Remark(author="Ashish" ,reviewBy="TL",shortDescription="This method builds Select statements from the from Clause.")
    public String selectQuery( String fromClause){ return null;}
}

Remark annotation applies for every class elements (variable, methods, constructor etc...) but this is not the applicable for all types of annotation.
There are specific annotations which can only be used in the context of annotations. These annotations are target, retention, documented and inherited.

Target Annotation


Target annotation helps to specify the applicability of the annotation on program elements. If a Target meta-annotation is not present on an annotation type declaration, the declared type may be used on any program element. If such a meta-annotation is present, the compiler will enforce the specified usage restriction.
@Target(ElementType[])
ElementType
TYPE
Class, interface, or enum (not annotation)
FIELD
Member field
METHOD
Class methods
PARAMETER
Parameters to the method
CONSTRUCTOR
Constructor
LOCAL_VARIABLE
Instance variable of the class
ANNOTATION_TYPE
Annotation types
PACKAGE
Java package

Retention Annotation


Retention annotation describes where and how long annotation is available.
@Retention(RetentionPolicy)
RetentionPolicy
SOURCE
Annotation available only for source code. Java compiler discards the annotation during the compilation time.
CLASS
Annotation available on .class file. Java class loader discards the annotation during the class loading time.
RUNTIME
Annotation available during the runtime.

Documentation annotation          

This annotation used to include the annotation related information in to the Java doc.
@Documented

Inherited annotation         


This annotation used to inherit the annotation thought the hierarchy. If annotation is not marked as @inherited then annotation applied for the super class does not inherit by the derived class.
@Inherited
Code snippet 4

Example
Define Query Type
package com.novice.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Query
{
    public String type() default "";
}

Create abstract SQL query class
package com.novice.examples;

import com.novice.annotation.Query;

@Query(type="SQL")
public abstract class SQLQuery
{
}


Implement abstract SQL query class into SQL Template
package com.novice.examples;
public class SQLTemplate extends SQLQuery
{
}

Test stub to demonstrate the purpose of @inherited annotation

public class TestInheritedAnnotation
{

    public static void main(String[] args)
    {
        Class<?> clz = SQLTemplate.class;
        Annotation[] annotations = clz.getAnnotations();
        for (Annotation annotation : annotations)
        {
            System.out.println(annotation);
        }
    }

}

Output
@com.novice.annotation.Query(type=SQL)

If you remove the @Inherited from the Query annotation then output is as follow
NO output … because annotation is not inherited on sub class. Query annotation is only available on abstract SQLQuery class.

           

This is all about to the Annotation… now it’s time to become ninja in Annotation.

Dissection of ORM


Now a days hibernate and other framework builds based on annotations, let’s try to understand how ORM tool works (explaining completely is out of scope of this blog but try to simulate the annotation based SQL statement generation)
Framework annotations
@Table
Describe the database table
Optional tablename property .if table name is not specify the class name consider as the table name
@Column
Describe the table column ,If columnname property is not specify then field  name consider as the column name
@PrimaryKey
Specify the primary key of the table

Code snippet 5

This class represents the User table.
/**
 * User.java
 * Created on Apr 16, 2011
 */
package com.novice.examples;


import com.novice.annotation.Column;
import com.novice.annotation.PrimaryKey;
import com.novice.annotation.Table;

/**
 * @author Ashish.Chudasama
 */
@Table
public class User
{

    @PrimaryKey(keyName = "user_id")
    String userId;

    @Column(columnName = "first_name")
    String firstName;

    @Column
    String lastName;

    @Column
    String dob;

    public void setUserId(String userId)
    {
        this.userId = userId;
    }

}

Code snippet 6
// import statements are remove from the code snippet
package com.novice.annotation;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table
{
    public String getTableName() default "";
}
Code snippet 7
// import statements are remove from the code snippet

package com.novice.annotation;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Column
{
   public String columnName() default "";
}

Code snippet 8
// import statements are remove from the code snippet
package com.novice.annotation;


@Target({ElementType.FIELD,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PrimaryKey
{
  public  String keyName();
}


Code snippet 9

This class generates the SQL statements by reading annotation from the requested pojo.

package com.ninja.annotationhelper;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

import com.novice.annotation.Column;
import com.novice.annotation.PrimaryKey;
import com.novice.annotation.Table;

public class ORMHelperTemplate
// I have avoided delegation of responsibility for the sake of example
    public String findAll(Class<?> clz) throws Exception
    {
        // First retrieve the from clause i.e. TableName
        Table annotation = clz.getAnnotation(Table.class);
        StringBuilder strSQLQuery = new StringBuilder();
        strSQLQuery.append("SELECT ");
        if (annotation != null)
        {
            // Retrieve all column name
            strSQLQuery.append(buildSelectColumn(clz.getDeclaredFields()));
            strSQLQuery.append(" From ");
            if (!annotation.getTableName().equals(""))
            {
                strSQLQuery.append(annotation.getTableName());
            }
            else
            {
                strSQLQuery.append(clz.getSimpleName());
            }

        }
        else
        {
            // invalid pojo class
throw new Exception("Invalid pojo class <missing @table Annotation>");
        }

        return strSQLQuery.toString();
    }
  

    private String buildSelectColumn(Field[] declaredFields) throws Exception
    {

        StringBuilder selectClause = new StringBuilder();
        for (Field field : declaredFields)
        {
            Annotation[] annotations = field.getAnnotations();
            for (Annotation annotation : annotations)
            {
                if (annotation instanceof Column)
                {
                    Column col = (Column) annotation;
                    if (!col.columnName().equals(""))
                    {
                        selectClause.append(col.columnName()).append(" , ");
                    }
                    else
                    {
                        selectClause.append(field.getName()).append(" , ");
                    }
                    break;
                }
                else if (annotation instanceof PrimaryKey)
                {
                    PrimaryKey pk = (PrimaryKey) annotation;
                    if (!pk.keyName().equals(""))
                    {
                        selectClause.append(pk.keyName()).append(" , ");
                    }
                    else
                    {
                        selectClause.append(field.getName()).append(" , ");
                    }
                    break;
                }
            }

        }
        if (selectClause.length() > 0)
        {
            return selectClause.substring(0, selectClause.lastIndexOf(" , ")).toString();
        }
        else
        {
throw new Exception("No Column to select < @PrimaryKey and/or @Column  annotation missing>");
        }
    }
// this method prepare SQL which retrieve records using PK.
    // for simplicity only string type is implemented as id
    public String findById(Object id) throws Exception
    {
        // First retrieve the from clause i.e. TableName
        Class<?> clz = id.getClass();
        Table annotation = clz.getAnnotation(Table.class);
        StringBuilder strSQLQuery = new StringBuilder();
        strSQLQuery.append("SELECT ");
        if (annotation != null)
        {
            // Retrieve all column name
            Field[] declaredFields = clz.getDeclaredFields();
            strSQLQuery.append(buildSelectColumn(declaredFields));
            strSQLQuery.append(" From ");
            if (!annotation.getTableName().equals(""))
            {
                strSQLQuery.append(annotation.getTableName());
            }
            else
            {
                strSQLQuery.append(clz.getSimpleName());
            }

strSQLQuery.append("  WHERE   ").append(buildWhereClause(declaredFields, id));
        }
        else
        {
            // invalid pojo class
throw new Exception("Invalid pojo class <missing @table Annotation>");
        }

        return strSQLQuery.toString();
    }

  // String special handling is not implemented
private String buildWhereClause(Field[] declaredFields, Object obj) throws Exception
    {
        StringBuilder selectClause = new StringBuilder();
        for (Field field : declaredFields)
        {
            Annotation[] annotations = field.getAnnotations();
            for (Annotation annotation : annotations)
            {
                if (annotation instanceof PrimaryKey)
                {
                    PrimaryKey pk = (PrimaryKey) annotation;
                    if (pk != null)
                    {
                        // actually we can used getter or setter
                        // for sake of demo i have used field access
                        boolean isAccessible = field.isAccessible();
                        field.setAccessible(true);
                        if (!pk.keyName().equals(""))
                        {
                            selectClause.append(pk.keyName()).append(" = ");

                        }
                        else
                        {
                            selectClause.append(field.getName()).append(" = ");
                        }

                        Object object = field.get(obj);
                        field.setAccessible(isAccessible);

                        if (object != null)
                        {
                            selectClause.append("'").append(object).append("' and ");
                        }
                        else
                        {
                            throw new Exception("PK Can't be null");
                        }

                    }
                    break;
                }
            }

        }
        if (selectClause.length() > 0)
        {
            return selectClause.substring(0, selectClause.lastIndexOf(" and ")).toString();
        }
        else
        {
throw new Exception("No Column to select < @PrimaryKey and/or @Column  annotation missing>");
        }
    }
}


Code snippet 10

Test the Custom ORM annotations

/**
 * TestORMHelper.java
 * Created on Apr 17, 2011
 */

package com.novice.examples;

import com.ninja.annotationhelper.ORMHelperTemplate;

/**
 * @author Ashish.Chudasama
 */
public class TestORMHelper
{

    public static void main(String[] args) throws Exception
    {
        ORMHelperTemplate template = new ORMHelperTemplate();
        System.out.println(template.findAll(User.class));
       
        User user=new User();
        user.setUserId("Ashis.Chudasama");
        System.out.println(template.findById(user));
    }

}

Output:

SELECT user_id , first_name , lastName , dob From User

SELECT user_id , first_name , lastName , dob From User Where user_id = 'Ashis.Chudasama'


I have just shown the power of annotation to build ORM tool. This is just the demo not the real ORM tool code snippets. In real life ORM tool uses may design pattern to process the user request as well as query parser parse the query and validate the query before generating database specific query.



2 comments: