Friday, 29 June 2012

Implementation of Rest service in Jira platform

REpresentational State Transfer (REST) is a style of software architecture for distributed systems such as web applications. REST has increasingly replaced other design models such as SOAP and WSDL due to its simpler style. Jira platform also gave a hook to create Rest service in it's architecture. Let's look, how to create a Rest service in Jira platform.

Problem Statement: Populate the list of priority of jira application using rest service.

1. First and the foremost point is, rest services can be created/implemented only in osgi plug-in (type-2) of jira.

Please add following xml code snippet in atlassian-plugin.xml file of type-2 plugin.

<!--             Application Rest Service             -->
    <rest key="app-rest" name="Application REST API" path="/application"    version="1.0">
        <description>Provides REST end points used in Application.</description>
    </rest>

2. Add a data source which would do the population of required object.


package com.application.company.gadget;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;


@Path("/fields")
@Produces(MediaType.APPLICATION_JSON)
public class CustomFieldDataResource extends AbstractGadgetResource {

    @GET
    @Path("/priority")
    public Keys getPriority() {
        List<String> priorities = this.getPriorities();
        Collection<Key> items = new ArrayList<Key>();
        for (final String priority : priorities) {
            items.add(new Key(priority, priority));
        }
        return new Keys(items);
    }
    
    private List<String> getPriorities() {
        Collection<Priority> systemPriorities = ComponentManager.getInstance().getConstantsManager()
                .getPriorityObjects();
        List<String> priorities = new ArrayList<String>();
        for (Priority priority : systemPriorities) {
            String priorityName = priority.getName();
            priorities.add(priorityName);
        }
        return priorities;
    }

   
    @XmlRootElement
    public class Keys {
        @SuppressWarnings("unused")
        @XmlElement
        private Collection<Key> keys;

        public Keys(Collection<Key> keys) {
            this.keys = keys;
        }
    }

    @XmlRootElement
    public class Key {
        @SuppressWarnings("unused")
        @XmlElement
        private String label;

        @SuppressWarnings("unused")
        @XmlElement
        private String value;

        public Key() {
        }

        Key(String label, String value) {
            this.label = label;
            this.value = value;
        }
    }

}


3. Now, build your type-2 plugin in jira platform and try to validate rest service.

Way to validate the rest service in generic form: 
http://<machine name>:<port-number>/<context>/rest/<rest-tag name>/1.0/<data-source-name>/<method/<behaviour name>

In our case, http://localhost:8887/jira/rest/application/1.0/fields/priority

You would see below response: 
{
  • "keys": [
    • {
      • "label": "P1",
      • "value": "P1"
      },
    • {
      • "label": "P2",
      • "value": "P2"
      },
    • {
      • "label": "P3",
      • "value": "P3"
      },
    • {
      • "label": "P4",
      • "value": "P4"
      },
    • {
      • "label": "P5",
      • "value": "P5"
      },
    • {
      • "label": "P6",
      • "value": "P6"
      },
    • {
      • "label": "Not Specified",
      • "value": "Not Specified"
      }
    ]
}


How to add a Listner in Jira platform

Though I have started blogging some time back in feb'12, I was not consistent in my posts. But, now onwards, I will try to post about jira platform consistently. Today, I will share about how listener can be configured in jira platform.

As literally speaking, LISTENER is some body/thing who listens and does the job based on what has happened on a particular event. It's the same on jira platform as well.

Jira has given one abstraction for the listener, AbstractIssueEventListener.java So, if you want to create a listener for your jira platform, you need to extend this class and implement the required events. Once you deploy your plug-in on the jira platform, by default this listener will be configured to your jira platform. You may see this on the administration section of jira application.


Ex: ApplicationListene.java

import com.atlassian.jira.event.issue.AbstractIssueEventListener;

public class ApplicationListener extends AbstractIssueEventListener {

   
    Logger logger = Logger.getLogger(ApplicationListener.class);
   
    @Override
    public void issueCreated(IssueEvent event) {
                   // do what ever you want do on create issue event
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of issue creation: ", e);
        }
    }
   
    @Override
    public void issueUpdated(IssueEvent event) {
        try {
              // what ever you want to do on the issue update         
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of issue update: ", e);
        }
    }
   
    @Override
    public void issueDeleted(IssueEvent event) {
        super.issueDeleted(event);
        try {
           // do some thing
           
        } catch (Exception e) {
            this.logger.error("Exception Caught while listening the event of issue delete", e);
        }
    }

    @Override
    public void issueMoved(IssueEvent event) {
        try {
            // what ever you want to do on moving issue event
        } catch (Exception e) {
            this.logger.error("Exception Caught while listening the event of issue move: ", e);
        }
    }

    @Override
    public void issueResolved(IssueEvent event) {
        try {
              // do some thing on issue resolution
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of issue resolve :", e);
        }
    }

    @Override
    public void issueClosed(IssueEvent event) {
        try {
             // do some thing on close event of the issue
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of issue close :", e);
        }
    }
   
    @Override
    public void issueReopened(IssueEvent event) {
        try {
             // do some thing on when issue is re-opened
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of issue reopen :", e);
        }
    }
   
    @Override
    public void issueWorklogDeleted(IssueEvent event) {
        try {
              // do some thing on when work log is deleted on the issue          
        } catch (Exception e) {
            logger.error("Exception Caught while listening the event of worklog deletion on issue :", e);
        }
    }

    @Override
    public void issueWorkLogged(IssueEvent event) {
        // Implement this to do some action on when work is logged on issue
    }

    @Override
    public void issueWorklogUpdated(IssueEvent event) {
        // Implement this to do some action on when worklog is updated on issue
    }
   
    @Override
    public void issueGenericEvent(IssueEvent event) {
        // Implement this to do some action on Issue Generic Event
    }
   
    @Override
    public void issueAssigned(IssueEvent event) {
        // Implement this to do some action on Issue Assigned Event
    }

    @Override
    public void issueCommented(IssueEvent event) {
        // Implement this to do some action on Issue Commented Event
    }

    @Override
    public void issueCommentEdited(IssueEvent event) {
        // Implement this to do some action on Issue Comment Edit event
    }
   
 }


Friday, 22 June 2012

How to add a jira service plugin in Jira platform

This post is about how to write a service in JIRA platform, which is responsible for executing a periodically task. One example could be pushing meta information of projects from jira platform to a stand-alone database for analysis or any other purpose.

I am assuming that, reader is familiar with development of type-2 plug-ins in jira platform.

Problem statement: Push meta information of projects in Jira platform to a stand-alone database.

1. SAL scheduler component needs to be imported into customized jira plugin by including following xml code snippet into atlassian-plugin.xml file.


<component-import key="pluginScheduler">
        <description>Atlassian SAL Scheduler</description>
        <interface>com.atlassian.sal.api.scheduling.PluginScheduler</interface>
</component-import> 



2. Create an abstract service class "DatabaseSyncService.java"


package com.company.application.schedule;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.sal.api.scheduling.PluginScheduler;
import com.
company.application.ao.service.EventLogService;

/**
 *
 *
 * @author sateesh.b
 *
 */
public abstract class DatabaseSyncService {

    private final PluginScheduler pluginScheduler;  // provided by SAL
    private ActiveObjects activeObject;
    private EventLogService eventLogService;
   
    /**
     * Constructor
     */
    public DatabaseSyncService(PluginScheduler pluginScheduler, ActiveObjects activeObject,
            EventLogService eventLogService) {
        this.pluginScheduler = pluginScheduler;
        this.activeObject = activeObject;
        this.eventLogService = eventLogService;
    }
   
    public ActiveObjects getActiveObject() {
        return this.activeObject;
    }
   
    public EventLogService getEventLogService() {
        return this.eventLogService;
    }
   
    public PluginScheduler getPluginScheduler() {
        return this.pluginScheduler;
    }
}
 


3. Sync Service interface

package com.company.application.schedule;

/**
 * This interface is used to schedule the services added in the application.
 *
 * @author sateesh.b
 *
 */
public interface SyncService {
   
    public void reschedule(long interval);
   
}


4. Create class which would register the service with the Jira Plugin Scheduler, so that it would run as a periodical service. This class should implement interface LifecycleAware, so that it would get called when this plugin is deployed in application server.


package com.company.application.schedule;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.sal.api.lifecycle.LifecycleAware;
import com.atlassian.sal.api.scheduling.PluginScheduler;
import com.
company.application.ao.service.EventLogService;
import com.
company.application.core.util.PropertyHelper;

/**
 * This is the component class, which adds <b>"Projects Meta Database Sync"</b> service to the
 * <code>JiraPluginScheduler</code>
 * 
 * This class is executed at the time of server startup
 * @author sateesh.b
 *
 */
public class
ProjectsMetaDatabaseSyncServiceImpl extends DatabaseSyncService implements SyncService, LifecycleAware {

    static final String KEY =
ProjectsMetaDatabaseSyncServiceImpl.class.getName() + ":instance";
    private static final String JOB_NAME = "
Projects Meta Info Database Sync";
   
    /**
     * constructor
     */
    public
ProjectsMetaDatabaseSyncServiceImpl (PluginScheduler pluginScheduler,
            ActiveObjects activeObject, EventLogService eventLogService) {
        super(pluginScheduler, activeObject, eventLogService);
    }
   
    /** 
     * Registering the "Projects Meta Info Database Sync" service in the application.
     * This service will run periodically with an interval of 1 day
     *
     */
    @Override
    public void reschedule(long interval) {
        getPluginScheduler().scheduleJob(JOB_NAME, ProjectsMetaInfoDatabaseSyncTask.class, new HashMap<String,Object>() {{
            put(KEY,
ProjectsMetaDatabaseSyncServiceImpl .this);}}, getServiceStartupDate(), interval);       
    }
   
   
    /** 
     * this method will be called at the start of the instance
     */
    @Override
    public void onStart() {
        //final String interval = PropertyHelper.getInstance().getPropertyValue("database.sync.interval");
        // interval information is fetched from property file
        final String interval = "60";        reschedule(Long.valueOf(interval));
    }   
   
    private Date getServiceStartupDate() {
        Calendar cal = Calendar.getInstance();
        GregorianCalendar gCal = new GregorianCalendar(cal.get(cal.YEAR), cal.get(cal.MONTH),
                cal.get(cal.DAY_OF_MONTH)+1, 0, 5);
        /*GregorianCalendar gCal = new GregorianCalendar(cal.get(cal.YEAR), cal.get(cal.MONTH),
                cal.get(cal.DAY_OF_MONTH), 15, 20);*/
        return gCal.getTime();
    }
}




5. Create a class which will handle the business logic of executing the repeatable task. This class needs to implement PluginJob.java interface as mentioned below. The below class will be invoked by the Jira Plugin Scheduler and "execute" behavior will be called with an interval mentioned in the class "ProjectsMetaDatabaseSyncServiceImpl.java"


/**
 *
 */
package com.company.application.schedule;

import java.sql.SQLException;
import java.util.Map;

import org.apache.log4j.Logger;

import com.atlassian.sal.api.scheduling.PluginJob;

/**
 * This <code>
ProjectsMetaInfoDatabaseSyncTask</code> class would connect to jira database to get the
 * jira project configurations and will be sync with stand-alone database
 *
 * This class is invoked by the <code>ServiceManager</code> of Jira framework.
 *
 * @author sateesh.b
 *
 */
public class
ProjectsMetaInfoDatabaseSyncTask implements PluginJob {
   
    private static Logger logger = Logger.getLogger(
ProjectsMetaInfoDatabaseSyncTask.class);
    private static boolean isRunning = false;
   
    /**
     * this method is invoked by the Jira Service manager
     */
    public void execute(Map<String, Object> jobDataMap) {
        if (!isRunning) {
            isRunning = true;
            try {
               
ProjectsMetaDatabaseSyncServiceImpl monitor = (ProjectsMetaDatabaseSyncServiceImpl) jobDataMap.get(ProjectsMetaDatabaseSyncServiceImpl.KEY);
                assert monitor != null;
            
            // now we can access the monitor object (ProjectsMetaDatabaseSyncServiceImpl) and write the
            // needful logic to get the project info and push them to stand-alone database.
            // errors can be logged into jira database 
                              
            } catch (ClassNotFoundException e) {
                logger.error("Unable to run database sync due to driver exception", e);
            } catch (SQLException e) {
                logger.error("Unable to run database sync due to sql exception", e);
            } finally {
                isRunning = false;
            }
        }
    }
}
 


6. Finally, we need to add the component/service plugin information into atlassian-plugin.xml file
Don't forget to mention this component as public (by mentioning public="true"), then only this component will be available with jira plugin service component.


   <component key="database-sync" class="com.company.application.schedule.ProjectsMetaDatabaseSyncServiceImpl"
             system="true" public="true">
        <description>The plugin component that schedules the projects meta info sync process.</description>
        <interface>com.atlassian.sal.api.lifecycle.LifecycleAware</interface>
        <interface>com.company.application.schedule.SyncService</interface>
    </component>


Wednesday, 15 February 2012

Exception after the implementation of Active Objects in Jira

2012-02-15 22:25:10,854 main ERROR      [sal.core.lifecycle.DefaultLifecycleManager] Error occurred while starting component 'com.company.application.ServiceImpl'. java.io.IOException: No such file or directory
java.lang.IllegalStateException: java.io.IOException: No such file or directory
        at net.java.ao.db.FileSystemUtils$1.get(FileSystemUtils.java:43)
        at net.java.ao.db.FileSystemUtils$1.get(FileSystemUtils.java:24)
        at com.google.common.base.Suppliers$MemoizingSupplier.get(Suppliers.java:93)
        at net.java.ao.db.FileSystemUtils.isCaseSensitive(FileSystemUtils.java:50)
        at net.java.ao.db.MySQLDatabaseProvider.isCaseSensetive(MySQLDatabaseProvider.java:143)
        at net.java.ao.schema.helper.DatabaseMetaDataReaderImpl.getTableNames(DatabaseMetaDataReaderImpl.java:46)
        at net.java.ao.schema.ddl.SchemaReader.readSchema(SchemaReader.java:73)
        at net.java.ao.schema.ddl.SchemaReader.readSchema(SchemaReader.java:61)
        at net.java.ao.schema.ddl.SchemaReader.readSchema(SchemaReader.java:52)
        at net.java.ao.schema.SchemaGenerator.generateImpl(SchemaGenerator.java:95)
        at net.java.ao.schema.SchemaGenerator.migrate(SchemaGenerator.java:68)
        at net.java.ao.EntityManager.migrate(EntityManager.java:143)
        at com.atlassian.activeobjects.internal.EntityManagedActiveObjects.migrate(EntityManagedActiveObjects.java:42)
        at com.atlassian.activeobjects.internal.AbstractActiveObjectsFactory.create(AbstractActiveObjectsFactory.java:52)
        at com.atlassian.activeobjects.internal.DelegatingActiveObjectsFactory.create(DelegatingActiveObjectsFactory.java:39)
        at com.atlassian.activeobjects.internal.RegistryBasedActiveObjectsProvider.get(RegistryBasedActiveObjectsProvider.java:29)
        at com.atlassian.activeobjects.osgi.DelegatingActiveObjects.getDelegate(DelegatingActiveObjects.java:126)
        at com.atlassian.activeobjects.osgi.DelegatingActiveObjects.migrate(DelegatingActiveObjects.java:31)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:597)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
        at org.springframework.osgi.service.importer.support.internal.aop.ServiceInvoker.doInvoke(ServiceInvoker.java:58)
        at org.springframework.osgi.service.importer.support.internal.aop.ServiceInvoker.invoke(ServiceInvoker.java:62)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:131)
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:119)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
        at org.springframework.osgi.service.util.internal.aop.ServiceTCCLInterceptor.invokeUnprivileged(ServiceTCCLInterceptor.java:56)
        at org.springframework.osgi.service.util.internal.aop.ServiceTCCLInterceptor.invoke(ServiceTCCLInterceptor.java:39)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
        at org.springframework.osgi.service.importer.support.LocalBundleContextAdvice.invoke(LocalBundleContextAdvice.java:59)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:131)
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:119)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
        at $Proxy900.migrate(Unknown Source)


Possible reasons for above mentioned stack:
a. "temp" directory is not created in the apache-tomcat home directory.
b. Accessibility issue with "temp" directory of apache-tomcat home directory. Check the read-write accessibility permissions.

SQL query to retrieve users who logged into Jira for a given duration


SELECT
    user_name     AS UserName,
    display_name  AS FullName,
    email_address AS Email
FROM 
    cwd_user cu
LEFT JOIN 
    (SELECT
          user_id, attribute_value
     FROM
          cwd_user_attributes
     WHERE
          attribute_name = 'login.lastLoginMillis' 
    ) login
ON
    cu.id = login.user_id
WHERE
    lastLogin.previousLogininMillis BETWEEN ? AND ? ORDER BY user_name

NOTE: Valid for Jira 4.3.3 version and above

Tuesday, 14 February 2012

Active Objects implementation in JIRA


Problem: Add created date and updated date states to Jira project entity, which is not present in Jira OOB.


This problem is addressed here by implementing Active Objects.


1. Download "activeobjects-plugin-0.15.4.jar" from https://repository.atlassian.com/com.atlassian.activeobjects/jars/.

2. Download "activeobjects-jira43-spi-0.15.4.jar" from https://repository.atlassian.com/com.atlassian.activeobjects/jars/.
Note: This plugin jar needs to be compatible with the Jira version. Here, this is compatible with Jira 4.3.3 version.

Deploy the above mentioned artifacts in the JIRA_HOME/plugins/installed-plugins folder. 

3. Import "Active Objects" component into your plugin. This is done by incorporating following snippet in the atlassian-plugin.xml file.

<component-import key="ao" name="Active Objects service" interface="com.atlassian.activeobjects.external.ActiveObjects">
      <description>Component to access Active Objects functionality from the plugin</description></component-import>


Note: Make sure to give component-import key as a two letter word.

4. Add the Transaction component in the plugin xml.

<component key="tx-processor" name="Transactional Annotation Processor"
        class="com.atlassian.activeobjects.external.TransactionalAnnotationProcessor">
        <decription>Processes @Transactional annotations.</decription>
</component>
 



By finishing the above mentioned steps,  it's good to go with adding the tables to Jira database.



5. We are adding the new states to default jira project entity by a new table. Create a table "Project_Ext" using active objects.
package ao.entity;

import java.sql.Timestamp;
import net.java.ao.Entity;



/**
 *
 * @author sateesh
 *
 */
public interface ProjectExt extends Entity {
   
    long getProjectId();
   
    void setProjectId(long projectId);
   
    Timestamp getCreatedDate();
   
    void setCreatedDate(Timestamp createdDate);
   
    Timestamp getLastUpdatedDate();
   
    void setLastUpdatedDate(Timestamp lastUpdatedDate);



    public static final String PROJECT_ID = "PROJECT_ID";
}

Also, add the below entry into atlassian-plugin.xml file
<ao key="ao-module">
      <description>The module configuring the Active Objects service used by this plugin</description>
<entity>com.company.application.ao.entity.ProjectExt</entity>

</ao>


6. Create a boot-strap service to migrate the custom tables (which are created using active objects). Below is are the two classes required to add to achieve this.

(i) BootStrapService

import com.atlassian.jira.extension.Startable;

/**
 *
 * The class implementing BootStrapService interface is notified when the JIRA application has started.
 * The start() method would be called after the plug-in system is initialized
 * and components added to the dependency injection framework, 
 * To be able to make it actually Startable, plug-in modules of type Component need to be created for it.
 *
 * @author sateesh
 *
 */
public interface BootStrapService extends Startable {
   
    public static final String PLUGIN_KEY = "xxxxxxxxx";
   
    public void initialize();
   
}


(ii) BootStrapServiceImpl


/**
 * Implementation of the Velocity Startable service.
 * It would be used to initialize the objects used by velocity platform.
 * e.g. - Manager classes, Cache, Listeners etc.
 * @author priyanka.jain & sateesh.b
 *
 */
public class BootStrapServiceImpl
implements BootStrapService{

    private final Logger logger = Logger.getLogger(BootStrapServiceImpl
.class);
   
    private ActiveObjects activeObject;
    

    /**
    * Adding the dependency of active object
    *
    */
    public BootStrapServiceImpl
(ActiveObjects activeObject) {
        this.activeObject = activeObject;
    }
   
    public void initialize() {
        logger.info("Initializing the My App Platform.");
        this.activeObject.migrate(ProjectExt.class);
    }

    public void start() throws Exception {
        try {
            initialize();
        } catch (ApplicationException e) {
            logger.error(e);
        }
    }


}



7. Add bootstrap service entries in the plugin-xml


<component key="my-bootstrap-service" name="App Bootstrap Service" class="com.company.application.BootStrapServiceImpl " public="true">
        <interface>
com.company.application.BootStrapService</interface>
 </component>



By doing this, ProjectExt table will be created in Jira database as "ao_e523d0_project_ext"
Here "ao" is the key mentioned in the atlassian-plugin.xml file for the active objects component, "e523d0" is the hash key for the plugin artifact and "project_ext" is the name of the entity class mentioned in the plugin artifact.

By implementing above mentioned steps, one can add new tables in Jira database using Active Objects. Now, let's focus on how to do crud operation on the tables using Active Objects.


8. Create ProjectExtService which will do crud operations on ProjectExt table.

(i) Add a value object class (wrapper class) for additional states of Jira project entity.
import java.sql.Timestamp;

/**
 * @author sateesh.b
 *
 */
public class ProjectExtVO {
   
    private long id;
    private Timestamp createdDate;
    private Timestamp updatedDate;



    public long getId() {
        return this.id;
    }


    public void setId(final long id) {
        this.id = id;
    }

   
    public Timestamp getCreatedDate() {
        return createdDate;
    }


    public void setCreatedDate(Timestamp createdDate) {
        this.createdDate = createdDate;
    }


    public Timestamp getUpdatedDate() {
        return updatedDate;
    }


    public void setUpdatedDate(Timestamp updatedDate) {
        this.updatedDate = updatedDate;
    }



}

(ii) Add an interface ProjectExtService
/**
 * Service for adding/updating/deleting project extension information
 * @author sateesh.b
 *
 */
public interface ProjectExtService
{
   
    /**
     * This method adds a new entry into the table
     * and takes input argument as {@link ProjectExtVO} and persists the data
     * @param projectExt
     * @return
     */
    public void add(ProjectExtVO projectExt);
   
    /**
     * this method updates existing entry in the table
     * and takes input argument as {@link ProjectExtVO} and persists the data
     * @param projectExt projectExt
     * @return
     */
    public void update(ProjectExtVO projectExt);
   
    /**
     * this method returns the project configuration information for a
     * given velocity project id
     *
     * @param pId
     * @return
     */
    public ProjectExtVO find(Long pId);
   
    /**
     * this method returns the list of project configuration information
     * @return
     */
    public List<ProjectExt> find();
   
    /** this method returns the list of project configurations for a given last updated date
     * @param lastUpdatedDate
     * @return
     */
    public List<ProjectExt> find(Timestamp lastUpdatedDate);
   
    /**
     * this method deletes the existing entry in the table
     * and takes input argument as long
     * @param id projectId
     * @return
     */
    public void delete(long id);


}



(iii) Add implementation class ProjectExtServiceImpl

/**
 * @author sateesh.b
 *
 */
public class ProjectExtService
Impl implements ProjectExtService{
   
    private final ActiveObjects activeObject;
   
    public ProjectExtService
Impl (ActiveObjects activeObject) {
        this.activeObject = activeObject;
    }
   
    @Override
    public void add(ProjectExtVO projectExt) {
        DBParam dbParam = new DBParam("PROJECT_ID"
, Long.valueOf(projectExt.getId())); 
        final ProjectExt project = this.activeObject.create(ProjectExt.class, dbParam);
        project.setCreatedDate(projectExt.getCreatedDate());
        project.setUpdatedDate(projectExt.setUpdatedDate());
        project.save();
    }

@Override
public void update(ProjectExtVO projectExt) {
        final ProjectExt[] projects = this.activeObject.find(ProjectExt.class,
                Query.select().where("PROJECT_ID = ?", projectExt.getId()));
        final ProjectExt project = projects[0];
        final ProjectExtVO projectExtension = getProjectExt(project);

        project.setUpdatedDate(projectExt.setUpdatedDate());
       project.save(); 
   }

@Override
    public ProjectExtVO find(Long id) {
        final ProjectExt[] projects = this.activeObject.find(ProjectExt.class,
                Query.select().where("PROJECT_ID = ?", id));
       
        ProjectExtVO projectConfig = null;
        if (projects.length > 0) {
            final ProjectExt project = projects[0];
            if (null != project) {

                projectConfig = new ProjectExtVO(); 
               projectConfig.setId(project.getProjectId());
                projectConfig.setCreatedDate(project.getCreatedDate());
                projectConfig.setUpdatedDate(project.getUpdatedDate());
            }
        }
        return projectConfig;
    }



@Override
    public void delete(long id) {
        final ProjectExt[] projects = this.activeObject.find(ProjectExt.class,
                Query.select().where("PROJECT_ID = ?", id));
        if (projects.length > 0) {
            final ProjectExt project = projects[0];
            this.activeObject.delete(project);   
        }
    }


Above mentioned 3 java classes would help to do CRUD operation on ProjectExt table which is created in step-5

9. Add the ProjectExtService entries in the atlassian-plugin.xml file.

<component key="project-ext-service" name="Project Service" class="com.company.appliation.ao.service.ProjectExtServiceImpl" public="true">
        <interface>com.company.appliation.ao.service.ProjectExtService</interface>

</component>