Developing Services in Kuali Student February 2011 Developing Services in Kuali Student 1 2 Developing Services in Kuali Student Contents 1 Introduction .......................................................................................................................................... 5 1.1 2 Understanding the service layer ........................................................................................................... 6 2.1 3 4 5 6 7 Requirements ................................................................................................................................ 5 Project Structure ........................................................................................................................... 7 Generating the Service API ................................................................................................................... 8 3.1 Checkout the ks-core-tutorial Project ........................................................................................... 8 3.2 Create the Service API ................................................................................................................... 9 3.3 Code Cleanup .............................................................................................................................. 10 Create JPA entities corresponding to API ........................................................................................... 13 4.1 Create initial “root” entity based on DTO ................................................................................... 14 4.2 Some JPA Quirks ......................................................................................................................... 20 4.3 JPA Entities Review ..................................................................................................................... 21 DAO Layer ........................................................................................................................................... 22 5.1 Create DAO interface .................................................................................................................. 22 5.2 Create DAO Tests ........................................................................................................................ 22 5.3 Named Queries ........................................................................................................................... 24 5.4 DAO Review ................................................................................................................................ 25 Service Implementation ...................................................................................................................... 26 6.1 Service Tests................................................................................................................................ 27 6.2 Service Implementation .............................................................................................................. 28 6.3 Exceptions ................................................................................................................................... 29 6.4 Extra Service Parameters ............................................................................................................ 29 6.5 Validation .................................................................................................................................... 30 6.6 Service Assemblers ..................................................................................................................... 30 6.7 Test Data ..................................................................................................................................... 32 6.8 Remaining Crud Operations ........................................................................................................ 34 6.9 Finishing Touches: Wiring in Dictionary, Validation and Search................................................. 37 6.10 Final steps ................................................................................................................................... 38 6.11 Service Implementation Review ................................................................................................. 43 Setting up Service Database ............................................................................................................... 45 Developing Services in Kuali Student 3 7.1 Schema Generation..................................................................................................................... 45 7.2 Export .......................................................................................................................................... 46 7.3 Adding Indexes to Foreign Keys .................................................................................................. 46 7.4 Generate SQL .............................................................................................................................. 47 7.5 Copy SQL Files to Your Project .................................................................................................... 47 7.6 Service Database Review ............................................................................................................ 47 8 Further reading / links to resources.................................................................................................... 48 4 Developing Services in Kuali Student 1 Introduction Kuali Student is comprised of several components including user interface, application and service layers. This document explains the service layer and how to create Java code for the API, service and persistence layers. 1.1 Requirements Before you start this tutorial, please make sure you have a solid understanding of the following concepts and technologies: Java RDB/SQL Spring Maven In addition, please follow the corresponding KS Developer Guide (https://wiki.kuali.org/display/KULSTG/Kuali+Student+System+Developer+Guide) to ensure that your environment is setup correctly. All software and tools installed (Java 6 JDK, Eclipse and plugins, Oracle XE, Maven, Subversion) KS projects already checked out and can run tests (ks-common, ks-core,ks-lum) Developing Services in Kuali Student 5 2 Understanding the service layer The service layer is designed like any typical 3-tier application. It consists of multiple layers: a clientfacing layer that uses SOAP, a service layer that handles the business logic and a persistence/DAO layer that interfaces with a database. SOAP SOAP stands for Simple Object Access Protocol. It is an XML protocol specification for exchanging structured information in the implementation of Web Services. We use the JAX-WS and JAXB APIs in conjunction with the CXF web service framework to handle the SOAP layer. Service DTOs These are plain old java objects (POJOs) that represent the data the service uses. They are annotated with JAXB annotations so they can be marshaled and unmarshaled into XML. Service Interface and Service Implementation These are responsible for business logic, data validation, and transforming data to and from the persistence layer. 6 Developing Services in Kuali Student JPA Entities These are JPA annotated POJOs that represent the data stored in a database. We use JPA as an object relational mapping technology to map from the POJOs to the database. DAOs These are responsible for CRUD operations (create, update, delete) as well as fetching and searching for data in the database. 2.1 Project Structure KS uses a standard directory layout for each service that is developed. The following example shows the KS Academic Time Period (ATP) service: ks-core Project ks-core-api/src/main/java org.kuali.student.core.atp.dto org.kuali.student.core.atp.service ks-core-api/src/main/resources META-INF wsdl ks-core-impl/src/main/java org.kuali.student.core.atp.dao org.kuali.student.core.atp.dao.impl org.kuali.student.core.atp.entity org.kuali.student.core.atp.service.impl ks-core-impl/src/main/resources ksb META-INF ks-core-impl/src/test/java org.kuali.student.core.atp.dao org.kuali.student.core.atp.service.impl ks-core-impl/src/test/resources Developing Services in Kuali Student DTOs (atpInfo POJOs) Service API interface Service WSDL DAO interface DAO implementation JPA Entitiy POJOs Service implementation Spring Context JPA persistence.xml configuration DAO Implementation tests Service Implementation tests Test data and configuration 7 3 Generating the Service API The service API represents the java service interface, the DTO POJOs, and the SOAP Web Service Definition Language(WSDL). In Kuali student, the service team will make a wiki page that defines the service. We use a tool to read that definition and create the Java DTOs and service interfaces. 3.1 Checkout the ks-core-tutorial Project This will be the template project we will use for our tutorial. Check out from svn: https://test.kuali.org/svn/student/examples/ks-core-tutorial/trunk 8 Developing Services in Kuali Student 3.2 Create the Service API 1. Download the ks-contract plugin: Check out via subversion and install into your local repo: https://test.kuali.org/svn/student/tools/maven-kscontract-plugin 2. Edit the pom of ks-core-api: Go to the URL: https://wiki.kuali.org/display/KULSTU/Academic+Time+Period+Service in your browser to obtain a valid jsession id if needed (In firefox go to Tools->Options->Privacy and click Remove individual cookies, then type wiki.kuali.org in the URL bar. Cut and paste the jsession Id into the test-kscontract pom) Make sure these are set in the pom as well: <packageName>org.kuali.student.core.atp</packageName> <serviceName>AtpService</serviceName> <namespace>http://student.kuali.org/wsdl/atp</namespace> <contractURL>https://wiki.kuali.org/display/KULSTU/Academic+Time+Period +Service</contractURL> These settings are used to generate the java code and all 4 parameters should be changed to match the service you are generating. 3. Run “mvn generate-sources”. This process will generate your Java interface, and all needed DTOs. 4. Once you have your service Java files, delete the contract config in your pom, or disable it by setting the phase to “none” Developing Services in Kuali Student 9 3.3 Code Cleanup 3.3.1 Extending TypeInfo Since KS has so many type structures that are very similar, there is a generic TypeInfo class you can extend to make life easier. Open AtpSeasonalType and change it to extend TypeInfo. Since TypeInfo contains the exact same structure as AtpSeasonalType, we can delete the majority of the code in AtpSeasonalType: @XmlAccessorType(XmlAccessType.FIELD) public class AtpSeasonalTypeInfo extends TypeInfo { private static final long serialVersionUID = 1L; } Some DTO types have additional fields. For example, the only difference between AtpTypeInfo and the generic TypeInfo is the two fields named seasonalType and durationType. Change AtpTypeInfo to extend TypeInfo and remove all the code except for seasonalType and durationType fields and accessor methods. Your code should look like this: @XmlAccessorType(XmlAccessType.FIELD) public class AtpTypeInfo extends TypeInfo implements Serializable, Idable, HasAttributes { private static final long serialVersionUID = 1L; @XmlElement private String durationType; @XmlElement private String seasonalType; /** * Unique identifier for an academic time period duration type. */ public String getDurationType() { return durationType; } public void setDurationType(String durationType) { this.durationType = durationType; } /** * Unique identifier for an academic time period seasonal type. */ public String getSeasonalType() { return seasonalType; } public void setSeasonalType(String seasonalType) { this.seasonalType = seasonalType; } } 10 Developing Services in Kuali Student Make similar changes to the remaining typeInfos (DateRangeTypeInfo and MilestoneTypeInfo). 3.3.2 Extending Search and Dictionary Make sure your Service extends SearchService and DictionaryService. These might not be on the wiki, but are needed. Ask the service team if you have any questions. public interface AtpService extends SearchService, DictionaryService { 3.3.3 Final Steps You may want to organize your imports (Ctrl-shift-O) to help clean up your code. Make sure that all your code compiles and nothing is missing or looks out of the ordinary as the contract plugin may have missed something. It is good to keep in mind that this project uses many different technologies and none of them are perfect. Many times you will follow directions and things will not work as expected. Developers are encouraged to research solutions to any issues and update our documentation/wiki/code so that others will benefit. 3.3.4 WSDL Generation At this point, you can generate your WSDL. The WSDL is an XML file that defines the SOAP contract. There is a CXF WSDL generator plugin for Maven that will generate our WSDL. 1. Open the pom.xml for the ks-core-api module. Look for the build plugin definition for cxf-java2wsplugin. Here you will add an additional execution for the service you are developing. <execution> <id>atp-wsdl</id> <phase>${ks.java2ws.phase}</phase> <configuration> <className>org.kuali.student.core.atp.service.AtpService</className> <serviceName>AtpService</serviceName> <targetNameSpace>http://student.kuali.org/wsdl/atp</targetNameSpace> </configuration> <goals> <goal>java2ws</goal> </goals> </execution> 2. Edit the serviceName, targetNameSpace, and className to match the similar values you used for creating the service using the contract plugin. Also make sure you have a unique Id for the execution. 3. Run the following on your ks-core-api pom: ks-core\ks-core-api>mvn -Dks.java2ws.phase=process-classes clean process-classes if you look in your project/ks-core-api/target/generated/wsdl, you will see your AtpService.wsdl Developing Services in Kuali Student 11 4. Copy this file to ks-core-api/src/main/resources/META-INF/wsdl Be sure to regenerate the WSDL if you make any further changes to the API so it remains in sync. 3.3.5 Service API Review We have just created everything we need for our service interface layer. This will allow developers to start coding against the service without writing any implementation code. The Service Team created a wiki page that defined our service contract. We ran the ks-contract-plugin to generate our java code. We extended TypeInfo in our Type DTOs to reduce code size and reusability. We made sure that SearchService and DictionaryService were extended if needed We double checked that the code was sound and copied it into our project structure. We added configuration to our api pom and generated the wsdl with the cxf-java2ws-plugin. 12 Developing Services in Kuali Student 4 Create JPA entities corresponding to API According to Wikipedia, “The Java Persistence API, sometimes referred to as JPA, is a Java programming language framework managing relational data in applications using Java.” We use JPA annotated POJOs to define a mapping between Java objects and the database. Our JPA provider is Hibernate, although in theory we should be able to use any JPA provider such as Eclipselink or OpenJPA. Developing Services in Kuali Student 13 4.1 Create initial “root” entity based on DTO Before creating entities, it is a good idea to become familiar with the entity diagram for the service and how the DTOs relate to one another. Let’s start by creating our first entity that corresponds to the AtpInfo object. We are now in implementation territory, so all ATP entities will go in the ks-core-impl module. 1. Create a new Class called “Atp.java” with a package of “org.kuali.student.core.atp.entity” and a superclass of “org.kuali.student.core.entity.MetaEntity”. All of our KS entity classes should extend “org.kuali.student.core.entity.BaseEntity”. MetaEntity extends BaseEntity and is used for any classes that have metaInfo(create and update time/date/user information) BaseEntity takes care of primary key ids and optimistic locking. As an added benefit we can easily extend all our entity classes if they all share the same super class. 2. All Annotated JPA entities need to be marked with @Entity at the class level. In addition, we need to define the table name that corresponds to this class by adding a @Table(name = "KSAP_ATP")annotation with the table name. IMPORTANT: Please use our naming conventions for all database column and table names. 3. Let’s also copy all of the fields from AtpInfo into our Atp entity class which we will edit later. Delete any @Xml annotations from the fields. At this point, your Atp class should look like this: @Entity @Table(name = "KSAP_ATP") public class Atp extends MetaEntity{ private String name; private RichTextInfo desc; private Date startDate; private Date endDate; private Map<String, String> attributes; private MetaInfo metaInfo; private String type; private String state; private String id; } 14 Developing Services in Kuali Student 4. Let’s remove some fields that our superclass is already taking care of. Delete the lines for “id” and “metaInfo” 5. Now let’s make sure that none of the field names are JPQL/SQL reserved words which can cause weird bugs later on. Change the field “desc” to “descr” Now we can start annotating our simple types (anything like String, int, long, Boolean) IMPORTANT: The KS convention is to add column names to every field so that we have control over the column names in the database. 6. Annotate the name and state fields with @Column(name = "NAME") @Column(name = "STATE") 7. For the start and end dates, we need to add additional annotations to tell the DB how we want our temporal fields to be stored (DateStamp,Timestamp,etc) Add The following to your startDate and endDate fields(with the correct corresponding column names): @Temporal(TemporalType.TIMESTAMP) @Column(name = "START_DT") 4.1.1 Dynamic attributes Many of the KS DTOs have dynamic attributes which are really a simple Map of string key to string value. To take advantage of some utility code, let’s add an interface to our class: implements AttributeOwner<AtpAttribute> Let’s also replace the code for the attributes field with the following: @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") private List<AtpAttribute> attributes; JPA does not support maps, so we need to convert our maps to a list of string-value pairs. Since many DTOs in KS have attributes, there are helper classes and interfaces. The @OneToMany annotation tells the JPA provider how to do joins between this class and the AtpAttribute class. Great! We only have two more fields to work with for now, descr and type. 4.1.2 Types Like attributes and meta, most KS entities have Types. In the database they exist as strings with some attributes of their own, but are mainly used by their ids as constraints on data. Types must be constrained to a set of values (you can just make up a new type, it has to be a value in the proper type table). Since we are using ORM, you can’t just set the string key of the type to a string value in the Developing Services in Kuali Student 15 referencing object—we need to set a specific “Type Object” value in the referencing Object. Let’s change the type definition to this: @ManyToOne @JoinColumn(name = "TYPE") private AtpType type; This constrains the type to an AtpType. 4.1.3 RichTextInfo RichTextInfo is another frequently used data type. We have a corresponding entity called RichText for that which can be extended each time you want a different RichText table. Currently we are making RichText tables per service, but you could do it per reference as well. @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "RT_DESCR_ID") private AtpRichText descr; This defines a relationship to an Atp-specific RichText table called AtpRichText. 4.1.4 Final Steps The Code should now look like this: @Entity @Table(name = "KSAP_ATP") public class Atp extends MetaEntity implements AttributeOwner<AtpAttribute> { @Column(name = "NAME") private String name; @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "RT_DESCR_ID") private AtpRichText descr; @Temporal(TemporalType.TIMESTAMP) @Column(name = "START_DT") private Date startDate; @Temporal(TemporalType.TIMESTAMP) @Column(name = "END_DT") private Date endDate; @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") private List<AtpAttribute> attributes; @ManyToOne @JoinColumn(name = "TYPE") private AtpType type; @Column(name = "STATE") private String state; } 16 Developing Services in Kuali Student We are done for now with our annotations and changes. Let’s generate our public getters and setters so we have a real Pojo. 4.1.5 Creating Missing Classes With our changes, we are now seeing some compilation errors due to use referencing classes that don’t yet exist: AtpAttribute AtpRichText AtpType In Eclipse you can right-click to create missing classes. Do this for the three missing classes as described in the sections below. 4.1.6 Attributes 1. Open up AtpAttribute. Let’s start with our JPA class-level annotations. Remember that each entity class needs an @Entity annotation. Also for KS we are explicitly defining table names using our standards. @Entity @Table(name = "KSAP_ATP_ATTR") 2. To make our new AtpAttribute class work with some of our utility classes, we will extend the Attribute class and give it the generic type Atp : public class AtpAttribute extends Attribute<Atp> { 3. You will now see that we are missing some method implementations. Let’s add some basic boilerplate code to complete the class. @ManyToOne @JoinColumn(name = "OWNER") private Atp owner; @Override public Atp getOwner() { return owner; } @Override public void setOwner(Atp owner) { this.owner = owner; } This adds an Atp field called Owner which maps back to an Atp. This is an example of bi-directional mapping. The @ManyToOne maps many AtpAttributes to one Atp.It also defines the column name Developing Services in Kuali Student 17 in the KSAP_ATP_ATTR table that will hold the id of the owning Atp. If you look at the Atp mapping for attributes, you will see the reverse of the mapping: @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") This maps a collection of AtpAttributes to a single Atp. It also uses the mappedBy attribute to define the field representing the owning entity on the non-owning side of the bidirectional relationship. In this case, the AtpAttribute “owns” the relationship since the AtpAttribute table contains the foreign key to the Atp. (The field name ‘owner’ is a little misleading in this case, as the Attribute is really the JPA owner) By default, relationships in JPA are unidirectional. If we want each side of the relationship to be aware of the other, we can make our relationship bi-directional. In bi-directional relationships we must set both sides of the relationship in out objects, for example: Atp atp = new Atp(); AtpAttribute attribute = new AtpAttribute(); atp.getAttributes().add(attribute); attribute.setOwner(atp); Ok! We’re finished mapping our first relationship. Let’s move on to the other missing classes. 4.1.7 RichText This is an easy one. Add our standard annotations of @Entity and @Table with a name attribute. Then all we have to do is extend the RichText class: @Entity @Table(name = "KSAP_RICH_TEXT_T") public class AtpRichText extends RichText { } If you look at the RichText class, you’ll see some new JPA annotations: @MappedSuperclass @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) This allows all subclasses to inherit the mappings from the superclass, and a new table will be created for each subclass. All we need is a new class that extends RichText, and we have a new RichText table. 4.1.8 Standard Types Think back to when we created the API DTOs for AtpTypes. We had a TypeInfo superclass that made life a little easier for us. There is a Type entity on the JPA side that corresponds to the TypeInfo class. 1. Let’s start on the AtpSeasonalType class. Create a new class called AtpSeasonalType. Let’s add @Entity and @Table annotations to it and extend Type<AtpSeasonalTypeAttribute>. This is similar to how we implemented the AttributeOwner interface. 18 Developing Services in Kuali Student At this point it might get a little overwhelming with all the classes we need. Our base entity Atp needed attributes, so we had to make an AtpAttribute class. Then we needed an AtpSeasonalType entity class which, in turn, needed another Attribute class. The service architects have determined that all types have dynamic attributes, so we will need to make these classes. 2. Edit your AtpSeasonalType to look like this: @Entity @Table(name = "KSAP_ATP_SEASONAL_TYPE") public class AtpSeasonalType extends Type<AtpSeasonalTypeAttribute>{ @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") private List<AtpSeasonalTypeAttribute> attributes; public List<AtpSeasonalTypeAttribute> getAttributes() { return attributes; } public void setAttributes(List<AtpSeasonalTypeAttribute> attributes) { this.attributes = attributes; } } 3. Now we need to make an AtpSeasonalTypeAttribute: @Entity @Table(name = "KSAP_ATP_SEASONAL_TYPE_ATTR") public class AtpSeasonalTypeAttribute extends Attribute<AtpSeasonalType> { @ManyToOne @JoinColumn(name = "OWNER") private AtpSeasonalType owner; @Override public AtpSeasonalType getOwner() { return owner; } @Override public void setOwner(AtpSeasonalType owner) { this.owner = owner; } } AtpSeasonalTypeAttribute and AtpAttribute are nearly identical. You can cut and paste the boilerplate code—just make sure you set a proper table name and change the GenericType. 4. Create AtpDurationType and AtpDurationTypeAttribute in the same manner. 4.1.9 Non Standard Types When we created AtpTypeInfo, we extended TypeInfo and added two additional fields for durationType and seasonalType. We need to reflect that data in the persistence entities. We will accomplish this by Developing Services in Kuali Student 19 extending Type (and creating a new AtpTypeAttribute class) and then adding two additional fields that have unidirectional relationships to the AtpDurationType and AtpSeasonalType classes. Let’s make AtpType just like we made AtpSeasonalType and AtpDurationType. Now let’s add the two additional fields that are in our DTOs and map them: @ManyToOne @JoinColumn(name = "SEASONAL_TYPE") private AtpSeasonalType seasonalType; @ManyToOne @JoinColumn(name = "DUR_TYPE") private AtpDurationType durationType; This is a ManyToOne unidirectional mapping. The @JoinColumn annotations define the column names on the KSAP_ATP_TYPE table that holds the foreign keys to seasonal and duration types. Add your getters and setters and we’re done. 4.1.10 Persistence.xml JPA requires a configuration file that defines where your entities are. 5. Create a new file called atp-persistence.xml in ks-core-impl/src/main/resources/META-INF <persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"> <persistence-unit name="Atp" transaction-type="JTA"> <class>org.kuali.student.core.entity.Type</class> <class>org.kuali.student.core.entity.Attribute</class> <class>org.kuali.student.core.entity.Meta</class> <class>org.kuali.student.core.entity.MetaEntity</class> <class>org.kuali.student.core.entity.RichText</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> </persistence-unit> </persistence> 6. Set the Persistence Unit Name to “Atp” which corresponds to our service. This groups the entities into a logical unit. The transaction-type sets whether or not this is a JTA supported persistence unit. The classes listed are the helper entities that we extended from. JPA providers can scan the classpath for JPA annotated classes, but this is time consuming and can cause problems if you have multiple persistence units. We use the exclude-unlisted-classes to explicitly list which classes are needed in out persistence unit. We will need to add the fully qualified class names of every class in our org.kuali.student.core.atp.entity package. (You can select the classes in the project explorer and cut and paste to make sure you don’t miss anything) 4.2 Some JPA Quirks 20 Developing Services in Kuali Student The following are some quirks of JPA and suggestions for how to mitigate them. 4.2.1 Mapping many-to-one primitives: There is no easy way in standard JPA 1.0 to map Collection<String> in an entity. You will have to make a new Entity that represents the String to map to. 4.2.2 Collection Initialization: In our DTOs, you will see in the getters: public List<AdminOrgInfo> getAdminOrgs() { if (adminOrgs == null) { adminOrgs = new ArrayList<AdminOrgInfo>(); } return adminOrgs; } This is fine and is needed for our xml binding to work in our DTO Info classes, but DO NOT use in Entities as it creates a variety of problems. 4.2.3 Naming issues and standards (sql conflicts) Please make sure you change the names of any fields that are reserved SQL/JPQL words. Use our Kuali Student naming standards for all table and column names. 4.3 JPA Entities Review We have just created our persistence Entities. This will allow developers to start work on the DAO layer, and start creating any test data that is needed. We created JPA annotated Entites corresponding to our DTOs We created attributes and types (and type attributes) We added relationship mapping between our entities We created a persistence.xml file that defines our persistence unit Developing Services in Kuali Student 21 5 DAO Layer The Data Access Object layer represents a layer between the services and the database. In Kuali Student, that makes it responsible for CRUD operations on JPA Entities, named query lookups, and searches. We will create a DAO interface, a JUnit test class, and the DAO implementation which will include JPQL named queries. 5.1 Create DAO interface The Dao Interface is pretty easy to make. Create an AtpDao interface and have it extend CrudDao and SearchableDao: public interface AtpDao extends CrudDao, SearchableDao { We will add more method signatures to this interface later as we need them. 5.2 Create DAO Tests KS tries to maintain test-driven development. This means that we try to write our tests before we write our implementation code. 22 Developing Services in Kuali Student KS has a test framework for our DAOs we will use to validate our entities and named queries. 1. Create a new Java class called org.kuali.student.core.atp.TestAtpDaoImpl: @PersistenceFileLocation("classpath:META-INF/atp-persistence.xml") public class TestAtpDaoImpl extends AbstractTransactionalDaoTest { @Dao(value = "org.kuali.student.core.atp.dao.impl.AtpDaoImpl") public AtpDao dao; @Test public void testCreateType() { } } @PersistenceFileLocation is an annotation that defines where our persistence.xml file is located. AbstractTransactionalDaoTest is our JUnit test framework class that takes care of all the Hibernate wiring, temporary data sources and transactions. The @Dao annotation injects an instance of the Dao Implementation into the dao field of our test class. It also is used to configure test data file locations which we will use later. 2. Let’s make our initial DAO implementation so we can run our test. In ks-core-impl/src/main/java, create a new class called org.kuali.student.core.atp.dao.impl.AtpDaoImpl and have it extend AbstractSearchableCrudDaoImpl and implement AtpDao. Add the following code to tie in the persistence unit to the entitymanager: public class AtpDaoImpl extends AbstractSearchableCrudDaoImpl implements AtpDao { @PersistenceContext(unitName = "Atp") @Override public void setEm(EntityManager em) { super.setEm(em); } } AbstractSearchableCrudDaoImpl is a DAO implementation that takes care of some basic CRUD operations and also our search framework. 3. Now that we have our Dao implementation we can run our test. Run the TestAtpDaoImpl as a JUnit test. If you are getting class not found errors, you might need to re-sync your environment with “mvn test-compile” first. Each DAO test is run in its own transaction that is rolled back at the end of the test, so any persistence changes in one test will not affect another test. Developing Services in Kuali Student 23 4. Our test is a little empty, so let’s add a quick check to see that we can persist and fetch a new AtpTpe: @Test public void testCreateType() throws DoesNotExistException { AtpType atpType= new AtpType(); atpType.setId("atp.type.1"); atpType.setName("Test Atp Type 1"); atpType = dao.create(atpType); AtpType foundAtpType = dao.fetch(AtpType.class, "atp.type.1"); assertEquals(atpType.getName(),foundAtpType.getName()); } This is typical of the kinds of tests we could run. The majority of our DAO tests are duplicated in the Service Tests, so it is up to the developer to decide how robust the DAO tests should be. You can see that the AtpDao already has CRUD operations (like create and fetch) in it that are inherited from the AbstractSearchableCrudDaoImpl. 5.3 Named Queries In the AtpService interface, there are many calls named get…(). The getAtp(String id) method can be implemented with a simple dao.fetch(AtpType.class, id); If we wanted to implement the getAtpsByAtpType(String atpTypeKey), we would need to create a database query that selects all Atps with a specific type. We will do this by writing a custom named query in JPQL, which is like SQL but works against the objects and not the tables. For the most part, the only other methods that needs to go into DAO implementations are calls to named queries. We define named queries as annotations on entity classes (usually the entity with the bulk of the returned columns). These usually correspond to a specific service “get” operations that return something other than standard dao fetch calls. 1. Add the following annotation code in Atp: @NamedQueries( { @NamedQuery(name = "Atp.findAtpsByAtpType", query = "SELECT atp FROM Atp atp WHERE atp.type.id = :atpTypeId") }) public class Atp extends MetaEntity implements AttributeOwner<AtpAttribute> { A few things to notice here: Please namespace your named queries so they are easy to identify and keep track of. Always qualify every field reference with an alias (atp.type.id instead of just type.id) Try to use our naming conventions, initial lowercase for aliases and binding variables, all caps for JPQL reserved words. 2. Let’s add a method signature to the AtpDao interface : 24 Developing Services in Kuali Student List<Atp> findAtpsByAtpType(String atpTypeKey); 3. Finally we need to implement the new method in AtpDaoImpl: @Override public List<Atp> findAtpsByAtpType(String atpTypeId) { Query q = em.createNamedQuery("Atp.findAtpsByAtpType"); q.setParameter("atpTypeId", atpTypeId); @SuppressWarnings("unchecked") List<Atp> results = q.getResultList(); return results; } The gist of this is we dereference the named query by the name, set the query parameters with the method parameters, and return the query results. 4. Now we should test our new method. Write a new test in TestAtpDaoImpl that creates two Atp types and three Atps, two of one type and one of the other. Then call findAtpsByAtpType with each of the types to make sure the JPQL is working. 5. Implement the remainder of your named queries to complete the DAO layer. Be sure to thoroughly test each query. 5.4 DAO Review We created our DAO interface and extended SearchableCrudDao to reuse code We created named queries for each comlex query in our service API We created a DAO implementation that extended AbstractSearchableCrudDaoImpl to make use of existing CRUD and search implementations. We created JUnit tests to test our DAO implementation. Developing Services in Kuali Student 25 6 Service Implementation The Service implementation is the business logic connecting the calls to the service with the persistence layer. The major code written in this layer has to do with converting DTOs into Entity beans. Other parts include validation and exception handling, and versioning. 26 Developing Services in Kuali Student 6.1 Service Tests Since we are doing test-driven development, we should start with a test. I like my first test to be a call to create. This usually involves the bulk of code you need to write. Create a new Junit test in ks-core-impl/src/test/java called org.kuali.student.core.atp.TestAtpServiceImpl. Have it extend AbstractService test and additional annotations so your code looks like this: @Daos( { @Dao(value = "org.kuali.student.core.atp.dao.impl.AtpDaoImpl")}) @PersistenceFileLocation("classpath:META-INF/atp-persistence.xml") public class TestAtpServiceImpl extends AbstractServiceTest { final Logger LOG = Logger.getLogger(TestAtpService.class); @Client(value = "org.kuali.student.core.atp.service.impl.AtpServiceImpl") public AtpService client; @Test public void testCreateAtp(){ } } AbstractServiceTest is part of our test framework that will start up an embedded datasource, inject your DAO implementation into your service implementation, start up a jetty server, and publish your service as a SOAP service. This lets you test end to end your service call to make sure there are no problems, such as marshalling errors, or strange persistence problems that would not get caught until later. Of course, you can feel free to create unit tests that use mock objects and avoid the overhead of integration testing. The @Daos and @PersistenceFileLocation annotations help the test framework know how to configure your DAO implementation. The @Client defines the service implementation class to use. Our test will create an Atp. Implement the testCreateAtp method using the client Info DTOs instead of the JPA Entities. Make sure you fill out every field and a dynamic attribute or two to make sure your test is working. Sample test: AtpInfo atpInfo = new AtpInfo(); atpInfo.setName("Atp one"); atpInfo.setDesc(new RichTextInfo()); atpInfo.getDesc().setPlain("Atp one Descr"); AtpInfo created = client.createAtp("atp.test.type.1", "atp.1",atpInfo); assertEquals(atpInfo.getName(),created.getName()); Developing Services in Kuali Student 27 assertEquals(atpInfo.getDesc().getFormatted(),created.getDesc().getForm atted()); This is the basic structure of the tests. We create data, perform a client operation, and then assert that the data matches our expected values. Make sure that if you are comparing values in collections, you don’t assert using index numbers like assertEquals(obj1.getList().get(0),obj2.getList().get(0));. This can cause intermittent bugs if you are not guaranteed the sort order of our two collections. 6.2 Service Implementation Once the test is finished, we need to start work on our Atp Service Implementation. In ks-coreimpl/src/main/java create a new class org.kuali.student.core.atp.service.impl.AtpServiceImpl which implements the AtpService interface. Give it a private AtpDao member(and set access method) so we have access to persistence: @WebService(endpointInterface = "org.kuali.student.core.atp.service.AtpService", serviceName = "AtpService", portName = "AtpService", targetNamespace = "http://student.kuali.org/wsdl/atp") @Transactional(readOnly=true,noRollbackFor={DoesNotExistException.class },rollbackFor={Throwable.class}) public class AtpServiceImpl implements AtpService { private AtpDao atpDao; In addition, there are two annotations here. @WebService is a JAX-WS annotation that helps define this class as implementing a web service. @Transaction is used with our spring configuration to mark this as a transactional class. The readOnly=true marks that all methods will be readOnly unless we specify otherwise, which we will need to do for methods that create, delete or update data. In our implementation of the createAtp method, we will add the @Transactional(readOnly=false) to mark this method as transactional. 28 Developing Services in Kuali Student 6.3 Exceptions The first things to notice are the exceptions that the service throws. Make sure you are handling these exceptions so that the service behaves as expected. Most service operations have missing parameter exceptions, so let’s add a check that see if the parameters passed in are null or empty and throw a MissingParameterException if any of them are. checkMissingParameters(new String[]{"atpTypeKey","atpKey","atpInfo"}, new Object[]{ atpTypeKey, atpKey, atpInfo}); checkMissingParameters is boilerplate code that should be added to a serviceUtils class in the future, but for now cut and paste the method from another service implementation private void checkMissingParameters(String[] paramNames, Object[] params) throws MissingParameterException { String errors = null; int i = 0; for(Object param:params){ if(param==null){ errors = errors==null?paramNames[i]:errors+", " + paramNames[i]; } i++; } if(errors!=null){ throw new MissingParameterException("Missing Parameters: " + errors); } } Some other exceptions you might see are VersionMismatchException which we will cover later, and CyclicDependencyException where you will have to check for cycles in a recursive object structure(like avoiding adding a group to itself). Try to implement the checks for each exception that might be thrown early so that they are not forgotten. The end users are expecting certain behavior based on the exceptions thrown. 6.4 Extra Service Parameters Another confusing thing about services is that some of the parameters are redundant. For example, the atpKey and atpTypeKey are both parameters, but they are both contained within the atpInfo object being passed in. In some cases the parameters might be used for lookup and the Info is used for replacement values. For now, let’s just assume that the parameters “win”, and use the parameters to set the corresponding values of the DTO: atpInfo.setType(atpTypeKey); atpInfo.setId(atpKey); Developing Services in Kuali Student 29 6.5 Validation The next thing we need to do is validate our object. This should be done for all operations that throw a DataValidationErrorException (mainly creates and updates). // Validate List<ValidationResultInfo> validationResults; try { validationResults = validateAtp("OBJECT", atpInfo); } catch (DoesNotExistException e) { throw new OperationFailedException("Validation call failed." + e.getMessage()); } if (null != validationResults && validationResults.size() > 0) { throw new DataValidationErrorException("Validation error.", validationResults); } 6.6 Service Assemblers Now that we know our data is valid, we need to use our DAO to persist it. The only problem is that we have a DTO object and our DAO takes in JPA entities. We’ll use an assembler class that does this transformation, and also does the reverse transformation. The rest of the createAtp code will now look like this: Atp atp = AtpAssembler.toAtp(new Atp(), atpInfo, atpDao); atpDao.create(atp); return AtpAssembler.toAtpInfo(atp); We’ll transform the DTO to an Entity, use our DAO to persist, and transform the result back to a DTO which is returned. 1. Make the AtpServiceAssembler class which extends BaseAssembler and the toAtp and toAtpInfo static methods. We will just copy the corresponding fields from the AtpInfo to the Atp and use the dao for looking up objects if needed. 2. Let’s start with the toAtp method. This is going to be used for both create and update so keep that in mind when implementing. Create will take in a blank Atp, and update will take in the currently persisted Atp. There are some helper utilities to assist in copying common data structures such as metaInfo, attributes, standard Types and richText. Let’s add code to copy attributes and richtext: // Copy Attributes atp.setAttributes(toGenericAttributes(AtpAttribute.class, atpInfo.getAttributes(), atp, atpDao)); //Copy RichText atp.setDescr(toRichText(AtpRichText.class, atpInfo.getDesc())); 30 Developing Services in Kuali Student 3. We need to do some additional logic when copying the type since we are only passed in the type key, and in JPA, we need to set the relationship to the type object. // Search for and copy the type try { AtpType atpType = atpDao.fetch(AtpType.class, atpInfo.getType()); atp.setType(atpType); } catch (DoesNotExistException e) { throw new InvalidParameterException("AtpType does not exist for key: " + atpInfo.getType()); } 4. All that is left is to copy the remaining “primitive” types: atp.setStartDate(atpInfo.getStartDate()); atp.setEndDate(atpInfo.getEndDate()); atp.setName(atpInfo.getName()); atp.setId(atpInfo.getId()); atp.setState(atpInfo.getState()); 5. We can take a shortcut with the previous code by replacing it with Spring’s BeanUtils to copy properties. BeanUtils.copyProperties(atpInfo, atp, new String[] { "type", "attributes", "metaInfo"}); Just remember to add the ignore properties for complex types, and other properties that you are copying manually or want to ignore. 6. Our toAtp method is complete, let’s make the toAtpInfo method. AtpInfo atpInfo = new AtpInfo(); BeanUtils.copyProperties(atp, atpInfo, new String[] { "type", "attributes", "metaInfo", "desc" }); // copy attributes, metadata, Atp, and Type atpInfo.setAttributes(toAttributeMap(atp.getAttributes())); atpInfo.setMetaInfo(toMetaInfo(atp.getMeta(), atp.getVersionNumber())); atpInfo.setType(atp.getType().getId()); atpInfo.setDesc(toRichTextInfo(atp.getDescr())); return atpInfo; You’ll see that this is very similar to the toAtpMethod. There are helper methods for attributes and Richtext. MetaInfo is also copied with a helper method (this is only needed one-way from the DB, metaInfo should never be updated manually) IMPORTANT: Try to keep the logic separated between the Service implementation and the Assemblers. Assemblers should only be responsible for transforming data and nothing more. Service code should not be doing any transformation that is not a special business case. Developing Services in Kuali Student 31 6.7 Test Data Run the test again to see how we’ve done. You should get this exception: org.kuali.student.core.exceptions.InvalidParameterException: AtpType does not exist for key: atp.test.type.1 What happened? Another confusing thing about our services is that there is no maintenance for types meaning the types must be created outside of the service. For testing this can be a pain. You can configure your tests to load test data prior to execution. Test data can exist as sql files or spring bean xml files. Let’s create test data that loads our AtpType into the DB. Create a file in ks-core-impl/src/text/resources called atp-test-beans.xml: <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="persistList" class="org.springframework.beans.factory.config.ListFactoryBean"> <property name="sourceList"> <list> <ref bean="atp.test.type.1" /> </list> </property> </bean> <bean id="atp.test.type.1" class="org.kuali.student.core.atp.entity.AtpType"> <property name="id" value="atp.test.type.1" /> <property name="name" value="Atp Test Type 1" /> <property name="descr" value="ATP Testing Type" /> <property name="effectiveDate" value="01/01/2008" /> <property name="expirationDate" value="01/01/2100" /> </bean> <bean id="customEditorConfigurer" class="org.springframework.beans.factory.config.CustomEditorConfigurer" > <property name="customEditors"> <map> <entry key="java.util.Date"> <bean class="org.springframework.beans.propertyeditors.CustomDateEditor"> <constructor-arg index="0"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="MM/dd/yyyy" /> </bean> </constructor-arg> <constructor-arg index="1" value="false" /> </bean> </entry> 32 Developing Services in Kuali Student </map> </property> </bean> </beans> This xml is a bit more complex than it needs to be, but the main parts of it are a bean with id=”persistList” that contains a list of JPA entities you would like inserted into persistence before your tests are run. The test framework looks for the bean named “persistList” and persists every bean in it (in order) using your DAO. The bean “atp.test.type.1” is just a spring representation of our Type object that will be persisted. (the customEditorConfigurer bean is not really important, it is just an easy way to add dates in your spring config) The last thing we need to do to get our test data working is to update our test class annotations to point to our test data file. In TestAtpService update the annotation so I looks like this: @Daos( { @Dao(value = "org.kuali.student.core.atp.dao.impl.AtpDaoImpl", testDataFile = "classpath:atp-test-beans.xml") }) Now run the test again and get no errors. 6.7.1 Implementing fetch operations: The bulk of getFoo() operations will use your DAOs and named queries. Let’s implement the getAtpsByAtpType method in the service that we already implemented in the DAO. 1. We’ll start with another test: Add another test for testing getAtpsByAtpType after the previous test: @Test public void testGetAtpsByAtpType() throws InvalidParameterException, MissingParameterException, OperationFailedException{ List<AtpInfo> atps = client.getAtpsByAtpType("atp.test.type.1"); assertEquals(1,atps.size()); assertEquals("Atp one",atps.get(0).getName()); } 2. Now we need to implement the service call. 2.1. First we’ll add a check for missing params as this method throws a MissingParameterException. 2.2. Next we’ll use the DAO to find the Atp entities that have the same type: atpDao.findAtpsByAtpType(atpTypeKey). 2.3. The last thing we need to do is transform the list of Atp entities into a list of AtpInfos. We can do this using a new assembler method toAtpInfoList, that will internally call toAtpInfo for each Atp passed in. @Override public List<AtpInfo> getAtpsByAtpType(String atpTypeKey) Developing Services in Kuali Student 33 throws InvalidParameterException, MissingParameterException, OperationFailedException { checkMissingParameters(new String[]{"atpTypeKey"}, new Object[]{atpTypeKey}); List<Atp> atps = atpDao.findAtpsByAtpType(atpTypeKey); return AtpAssembler.toAtpInfoList(atps); } And in AtpAssembler: public static List<AtpInfo> toAtpInfoList(List<Atp> atps) { if(null==atps){ return Collections.<AtpInfo>emptyList(); } List<AtpInfo> atpInfoList = new ArrayList<AtpInfo>(atps.size()); for (Atp atp : atps) { atpInfoList.add(toAtpInfo(atp)); } return atpInfoList; } 3. Now you can run your test again, and the service will call your custom DAO method which will execute the named query and then transform the results back to a list of DTOs. This works because you have already created an Atp in the previous test. The service tests differ from the Dao tests in that the service tests don’t rollback changes so be sure that each test can be run independently (i.e. our last test is bad because it will fail if run before the testCreateAtp test). JUnit DOES NOT GUARANTEE the order in which tests are run, so your tests should never rely on data manipulated in another test. One way to ensure this is to delete any data that was created in the same test. A testCrud method shouldcreate an entity, update it and finally delete it. What we really should be doing is adding test data either with SQL or Spring Beans that we can test our get methods with. 6.8 Remaining Crud Operations 6.8.1 Updates 1. Rename the testCreateAtp test to testAtpCrud. We are going to test create, delete and update methods in one test so the DB state is unchanged after a successful test. Let’s add some code to test updates. 2. After creating the ATP, change every field slightly so we can make sure that the fields have been changed correctly. Then call client.updateAtp() to perform the update. At this point we should also check for version mismatchExceptions(optimistic locking): 34 Developing Services in Kuali Student (modify Atp values here) //Update Atp AtpInfo updatedAtp = client.updateAtp(“someAtpKey…”, createdAtp); //now try to update again with the same version try { client.updateAtp(atp_fall2008Semester, createdAtp); fail("AtpService.updateAtp() should have thrown VersionMismatchException"); } catch (VersionMismatchException vme) { // what we expect } Make sure you have assertion tests for every value to make sure your Assembler is copying correctly. 3. It’s time to implement the update method in the service implementation. 3.1. Check for missing params 3.2. Load the existing atp from persistence 3.3. Check for VersionMismatchException 3.4. Use the assembler to transform the AtpInfo values into the existing Atp 3.5. Transform the resulting Atp back to an AtpInfo and return 6.8.2 Optimistic Locking Version mismatch Exceptions are caused by optimistic locking. Every time an entity is updated, the @Version annotation tells our persistence provider to increment the optimistic lock field on our object. If User1 loads an Atp for editing and then User2 loads the same Atp, if user1 makes changes and saves and then User 2 makes other changes and saves, User 2 would overwrite the changes that User1 made. To prevent this from happening, the version indicator of the DTO is compared to the Entity in persistence. If they do not match, the VersionMismatchException is thrown, forcing User2 to reload the Atp to make any changes. Be careful not to confuse the optimistic lock ‘version’ with the term ‘version’ as it relates to historical data. We hope to change this terminology in the future to make it less confusing. Developing Services in Kuali Student 35 6.8.3 Completion of Update Code The Final code should look like this: @Override public AtpInfo updateAtp(String atpKey, AtpInfo atpInfo) throws DataValidationErrorException, DoesNotExistException, InvalidParameterException, MissingParameterException, OperationFailedException, PermissionDeniedException, VersionMismatchException { CheckMissingParameters(new String[]{"atpKey","atpInfo"}, new Object[]{ atpKey, atpInfo}); Atp atp = atpDao.fetch(Atp.class, atpKey); if (!String.valueOf(atp.getVersionNumber()).equals(atpInfo.getMetaInfo().getVers ionInd())){ throw new VersionMismatchException("Atp to be updated is not the current version"); } atp = AtpAssembler.toAtp(atp, atpInfo, atpDao); Atp updatedAtp = atpDao.update(atp); return AtpAssembler.toAtpInfo(updatedAtp); } By separating out the copying and persistence to other classes, our service implementation becomes very simple and readable. 6.8.4 Update List Pattern Sometimes you will need to perform updates on a list of items. Instead of deleting the entire list and recreating it, it is sometimes more optimal to compare the values in the existing list and the new values to be persisted. This way you can add anything missing, update anything that is in both lists and delete anything that is left over. You can see this in use in the BaseAssembler.toGenericAttributes() method. The existing list is copied into a map keyed by the unique id of the items in the existing list, then the existing list is cleared out. Next we iterate over the new list and check if the key exists in our map. If the key exists we do an update of the existing value and remove that entry from the map, if it does not we need to create a new object. At this point our existing list will contain all pre-existing and new items from the input. The last thing we will do is clean up the orphaned items that are left in the map by deleting them. 6.8.5 Updates that cause deletes Some objects have dependent objects related to them that must be deleted if the relationship is deleted during an update. For example, Clu contains a list of CluIdentifiers. If the persisted Clu had 2 Identifiers, CluIdent1 and CluIdent2, and we are updating it to contain only the new identifier CluIdent3, we will have to explicitly delete CluIdent1 and CluIdent2, then add CluIdent3, so there are not any orphaned CluIdentifiers 36 Developing Services in Kuali Student floating around the database. This logic could be placed in the Service implementation (not ideal), the Assembler or, possibly, the assembler could return a list of all the orphaned entities and leave the responsibility of deleting them to the service implementation. 6.8.6 Deletes Deletes are relatively simple to do, just call dao.delete on your entity. Most delete operations return a StatusInfo Object which we just set to true and return. 6.9 Finishing Touches: Wiring in Dictionary, Validation and Search 6.9.1 Dictionary We will delegate all of our dictionary calls to an existing DictionaryService. Add a member called dictionaryService and accessor methods: private DictionaryService dictionaryService; We only need to implement two methods for the DictionaryService interface and we can delegate those: @Override public ObjectStructureDefinition getObjectStructure(String objectTypeKey) { return dictionaryServiceDelegate.getObjectStructure(objectTypeKey); } @Override public List<String> getObjectTypes() { return dictionaryServiceDelegate.getObjectTypes(); } 6.9.2 Search We can delegate calls to search just like the dictionary using a search manager. Add a member and accessor methods for searchManager: private SearchManager searchManager; Implement the remaining SearchService methods by delegating to the search manager. 6.9.3 Validation We will also use delegation for validation, but there is some additional logic we will need. Add a ValidatorFactory member: private ValidatorFactory validatorFactory; Implement each validation method by delegating to the validator factory and passing in the proper key. Our implementation uses the class name as the object structure key used for validation: @Override public List<ValidationResultInfo> validateAtp(String validationType, Developing Services in Kuali Student 37 AtpInfo atpInfo) throws DoesNotExistException, InvalidParameterException, MissingParameterException, OperationFailedException { checkMissingParameters(new String[]{"validationType", "atpInfo"}, new Object[]{ validationType , atpInfo}); ObjectStructureDefinition objStructure = this.getObjectStructure(AtpInfo.class.getName()); Validator defaultValidator = validatorFactory.getValidator(); List<ValidationResultInfo> validationResults = defaultValidator.validateObject(atpInfo, objStructure); return validationResults; } It is beyond the scope of this training to configure search and data dictionaries. Please review the configuration guides. In the meantime, you can temporarily disable the validation until the data dictionary is complete. 6.10 Final steps 6.10.1 Wiring in new service to application KS uses Spring and the inversion of control pattern extensively. This makes it easy to build up an application from many parts. 1. 2. 3. 4. We’ll need a datasource which should already be configured. Configure the EntitymanagerFactory and EntityManagers using the datasource. Configure our DAO and plug in the EntityManager. Configure our service and plug in our Dao. Other parts we need are wired in to our service as well including the dictionary, search, and validation. 5. Finally, we expose our service to the KSB bus as a soap service. 38 Developing Services in Kuali Student Developing Services in Kuali Student 39 Atp should be configured in ks-core-context.xml . These beans should already be configured, especially the core datasource, default entityManagerFactory, validationFactory, DicttionaryService and searchDispatcher: <bean id="coreDataSource" class="bitronix.tm.resource.jdbc.PoolingDataSource" init-method="init" destroy-method="close"> <property name="className" value="oracle.jdbc.xa.client.OracleXADataSource" /> <property name="uniqueName" value="coreDataSource" /> <property name="maxPoolSize" value="${ks.core.datasource.maxSize}" /> <property name="useTmJoin" value="true" /> <property name="testQuery" value="${ks.core.datasource.validationQuery}" /> <property name="allowLocalTransactions" value="true" /> <property name="driverProperties"> <props> <prop key="URL">${ks.core.datasource.url}</prop> <prop key="user">${ks.core.datasource.username}</prop> <prop key="password">${ks.core.datasource.password}</prop> </props> </property> </bean> <!-- Default JPA EntityManagerFactory --> <bean id="coreDefaultEntityManagerFactory" abstract="true" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="databasePlatform" value="${ks.core.jpa.DatabasePlatform}" /> <property name="showSql" value="${ks.core.jpa.showSql}" /> <property name="generateDdl" value="${ks.core.jpa.generateDdl}" /> </bean> </property> <property name="jpaPropertyMap"> <map> <entry key="hibernate.transaction.manager_lookup_class" value="${ks.core.jpa.JpaProperties.hibernate.transaction.manager_lookup_class }"/> <entry key="hibernate.hbm2ddl.auto" value="${ks.core.jpa.JpaProperties.hibernate.hbm2ddl.auto}"/> <entry key="hibernate.connection.release_mode" value="${ks.core.jpa.JpaProperties.hibernate.connection.release_mode}"/> <!--<entry key="hibernate.connection.autocommit" value="${ks.core.jpa.JpaProperties.hibernate.connection.autocommit}"/>--> </map> </property> </bean> <bean id="coreDictionaryService" 40 Developing Services in Kuali Student class="org.kuali.student.core.dictionary.service.impl.DictionaryServiceImpl"> <constructor-arg index="0" value="${ks.core.dictionary.serviceContextLocations}" /> </bean> <bean id="coreServiceValidator" class="org.kuali.student.common.validator.DefaultValidatorImpl"> <property name="searchDispatcher" ref="coreSearchDispatcher"/> </bean> <bean id="coreValidatorFactory-parent" abstract="true" class="org.kuali.student.common.validator.ValidatorFactory"> <property name="defaultValidator" ref="coreServiceValidator"/> <property name="validatorList"> <list/> </property> </bean> <bean id="coreValidatorFactory" parent="coreValidatorFactory-parent"/> <bean id="coreSearchDispatcher" class="org.kuali.student.core.search.service.impl.SearchDispatcherImpl"> <property name="services"> <list> <ref bean="atpServiceImpl"/> <ref bean="emServiceImpl"/> <ref bean="orgServiceImpl"/> <ref bean="documentServiceImpl"/> <ref bean="proposalServiceImpl"/> <ref bean="commentServiceImpl"/> </list> </property> </bean> The SearchDispatcher is used when performing a search where the target service is unknown, only the search key is known. You will need to add the atpServiceImpl to the search dispatcher for it to properly dispatch searches to the Atp service. Developing Services in Kuali Student 41 TherRest of the configuration will need to be added: <!-- Atp Service Config --> <bean id="atpEntityManagerFactory" parent="coreDefaultEntityManagerFactory"> <property name="persistenceUnitName" value="Atp"/> <property name="persistenceXmlLocation" value="classpath:METAINF/atp-persistence.xml" /> <property name="dataSource" ref="coreDataSource" /> </bean> <bean id="atpEntityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean"> <property name="entityManagerFactory" ref="atpEntityManagerFactory" /> </bean> <bean id="atpDao" class="org.kuali.student.core.atp.dao.impl.AtpDaoImpl"> <property name="em" ref="atpEntityManager" /> </bean> <bean id="atpServiceImpl" class="org.kuali.student.core.atp.service.impl.AtpServiceImpl"> <property name="atpDao" ref="atpDao" /> <property name="searchManager" ref="atpSearchManager"/> <property name="dictionaryService" ref="coreDictionaryService"/> <property name="validatorFactory" ref="coreValidatorFactory"/> </bean> <bean id="atpSearchManager" class="org.kuali.student.core.search.service.impl.SearchManagerImpl"> <constructor-arg index="0" value="classpath:atp-search-config.xml" /> </bean> <bean id="ks.exp.atpService" class="org.kuali.rice.ksb.messaging.KSBExporter"> <property name="serviceDefinition"> <bean class="org.kuali.rice.ksb.messaging.SOAPServiceDefinition"> <property name="jaxWsService" value="true" /> <property name="service" ref="atpServiceImpl" /> <property name="serviceInterface" value="org.kuali.student.core.atp.service.AtpService" /> <property name="localServiceName" value="AtpService" /> <property name="serviceNameSpaceURI" value="http://student.kuali.org/wsdl/atp" /> <property name="busSecurity" value="${ks.core.bus.security}" /> </bean> </property> </bean> The service specific config is fairly self explanatory. We use property placeholders where possible to make configuration as easy as possible for end users. Each layer of our stack is instantiated and plugged into the next layer. 42 Developing Services in Kuali Student The SharedEntityManagerBean is a way to avoid using JNDI in obtaining a reference to the entity manager. KSBExporter allows us to publish services on the KSB (Kuali Service Bus). We use the SOAPServiceDefinition with jaxWsService set to “true” to publish as a SOAP endpoint. Internally KSB uses CXF to accomplish this. 6.10.2 Note on service testing and additional spring context: Our test framework for services only injects daos, so if you need to inject other objects into the service implementation during testing, you can configure them in an additional spring context file and add an annotation attribute to your test. The beans in the additional context will be automatically wired by type into your service implementation. 1. Create a new context file in ks-core-impl/src/test/resources called atp-additional-context.xml: <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="searchManager" class="org.kuali.student.core.search.service.impl.SearchManagerImpl" autowire="byType"> <constructor-arg index="0" value="classpath:atp-search-config.xml" /> </bean> <bean id="dictionaryService" class="org.kuali.student.core.dictionary.service.impl.DictionaryServiceImpl"> <constructor-arg index="0" value="classpath:ks-atp-dictionarycontext.xml" /> </bean> <bean id="atpServiceValidator" class="org.kuali.student.core.dictionary.service.MockDefaultValidatorImpl"/> <bean id="validatorFactory" class="org.kuali.student.common.validator.ValidatorFactory"> <property name="defaultValidator" ref="atpServiceValidator"/> </bean> </beans> 2. Now add the following @Client attribute to your TestAtpSerrviceImpl test class: additionalContextFile="classpath:atp-additional-context.xml" This will configure your test to inject any other beans your service needs. You can use mock objects or real ones, it’s up to you. 6.11 Service Implementation Review We have now completed the final layer to our service stack. Developing Services in Kuali Student 43 44 We created a test class and test cases for our service We created an Assembler to transform DTOs into JPA entities and back We implemented the Crud operations in our service We implemented out fetch operations using our DAO which called named queries We added support for Validation, Dictionary and Search by delegation We wired up our service using spring configuration. Developing Services in Kuali Student 7 Setting up Service Database The schema for our database must be put into the project so that it is available to users. Fortunately, Hibernate can automatically generate the schema for us. All we have to do is run our application while enabling the generation of the schema, run the IMPEX Export tool and copy the relevant SQL to our project. 7.1 Schema Generation Once your service is configured to run in the application, all we need to do is set the hibernate config param: hibernate.hbm2ddl.auto to update. You can see this is being set in the ks-core-context.xml bean definition for the coreDefaultEntityManagerFactory. Edit your ${user}/kuali/main/dev/ks-embedded-config.xml and set these properties: <param name="ks.core.jpa.JpaProperties.hibernate.hbm2ddl.auto">update</param> <param name="ks.core.jpa.generateDdl">true</param> Now run your ks-embedded war as you normally would. The schema will be created in your database. Developing Services in Kuali Student 45 7.2 Export Run the Kuali IMPEX export task, making sure that you have configured your impex-build.properties file to point to the correct database and project. For example: export.torque.database.user=KSEMBEDDED export.torque.database.schema=KSEMBEDDED export.torque.database.password=KSEMBEDDED … torque.schema.dir={your project directory}/ks-cfg-dbs/ks-standalone-db/src/main/impex 7.3 Adding Indexes to Foreign Keys As a rule of thumb, it is important to add indexes to all foreign key columns. If you don’t, Oracle performance will suffer and you might get deadlocks during referential integrity checks. 1. In your ks-cfg-dbs (or wherever you exported your data to), look in the impex/schema.xml file. This is where Impex stores the schema data. Look for the table definition for KSAP_ATP. You should see these foreign key constraints: <foreign-key foreignTable="KSAP_RICH_TEXT_T" name="FK123098231BB"> <reference foreign="ID" local="RT_DESCR_ID"/> </foreign-key> <foreign-key foreignTable="KSAP_ATP_TYPE" name="FK123098231BA"> <reference foreign="TYPE_KEY" local="TYPE"/> </foreign-key> 2. First, the names of the foreign keys are not very useful and are autogenerated. Let’s rename them so we know what table they belong to. <foreign-key foreignTable="KSAP_RICH_TEXT_T" name="KSAP_ATP_FK2"> <reference foreign="ID" local="RT_DESCR_ID"/> </foreign-key> <foreign-key foreignTable="KSAP_ATP_TYPE" name="KSAP_ATP_FK1"> <reference foreign="TYPE_KEY" local="TYPE"/> </foreign-key> 3. Let’s add indexes to the local columns that reference foreign tables right below these definitions: <index name="KSAP_ATP_I1"> <index-column name="TYPE"/> </index> <index name="KSAP_ATP_I2"> <index-column name="RT_DESCR_ID"/> </index> 4. Now our data should look and perform better. 46 Developing Services in Kuali Student 7.4 Generate SQL After running export, you can run the import task on your impex project to create the SQL data files you will need. You might want to clean out the sql and datasql folders first if they are causing conflicts. 7.5 Copy SQL Files to Your Project In your ks-cfg-dbs (or wherever you exported your data), look in the impex/sql/schema.sql file. This is where the schema was created. Search for your new module’s table definitions (KSAP_*) and cut and paste them all into a new .sql file located in the sql module of your project (ks-core-sql/src/main/resources). Depending on whether this is a new module, or a new service in an existing module you will want to add the sql file to the initial-db or upgrades folders. You will also want to copy the constraints for KSAP* tables for the Impex project in schema-constraints.sql. If there is any data that your module requires, you should copy/create that as well. 7.6 Service Database Review Once you commit your sql files to the sql module, the data will be built into the latest KS impex project for everyone to use. We generated the schema using Hibernate’s ddl generation We exported our database to an impex project We cleaned up our schema names and added indexes to foreign keys We used impex again to generate sql code We copied our new module’s schema, constraints and data to our project. Developing Services in Kuali Student 47 8 Further reading / links to resources JPQL reference: http://download.oracle.com/docs/cd/E11035_01/kodo41/full/html/ejb3_langref.html JPA annotation reference: http://www.oracle.com/technetwork/middleware/ias/toplink-jpa-annotations-096251.html Spring reference: http://static.springsource.org/spring/docs/2.5.x/reference/ KS Documentation (Specifically for Configuration Guide and Developer Guide): https://wiki.kuali.org/display/KULSTG/KS+Curriculum+Management+1.1+Documentation 48 Developing Services in Kuali Student