Multiple service.xml Files in a Single Plugin

I think before we get rolling here I need to first admit that blogging about 6.2 features and functions is maybe not the best practice since we should all be planning our move to 7 in preparation for the end of days that are coming. In truth, I have wanted to write this post since before Liferay 7/DXP hit the stage but never had the time. With that said, I think that there are still A LOT of people both today and in the future who will remain on 6.2 and may benefit from some of this information. So -- this post is for you :). I should also add that this might be something that you can do in 7 as well, though I think the new architecture probably makes this feature a little less sexy. 

I'm going to say something now that some of you will boo. Ready? I'm a fan of Service Builder. It's not the perfect solution, but if you have spent the time to study the hard benefits, and then also consider the soft benefits, then I'm sure you will agree that it is not the most evil thing ever produced. It's not perfect, but nothing is and most of the time the hardship it brings is the result of misuse. As much as I love service builder though there is one thing that always bothered me about it -- In particular, the fact that I can only have ONE package configuration for all my entities. In this post I want to look at this aparent limitation, and then show an (undocumented? to my knowledge at least) build parameter that can be used to remove the limitation.

 

Setting the Stage


Ok -- let's start with the black and white. You need to write 5 portlets. Some people will put them all in one plugin (5 portlet descriptors in the one plugin package), others will make one plugin project per portlet. I've witnessed and participated in many a debates over which solution is better. I'm not writing this to debate what approach is best. There are lots of compelling arguments for both patterns from modularity, to data sharing, to resource usage, and more. Personally, I think a hybrid of the two is what's (often) best. Let's say 3 of my 5 portlets are related enough that it makes sense to keep them together, and that the other 2 warrant their own projects. In the case of the individual plugins? one service.xml file with your entity deinitions, all under one package, makes sense. For the case where you have 3 portlets in one plugin project, all needing service builder entities -- you're still using one package to store everything. But I don't want that. I want a pacakge for each of my entities.

Let's be honest here, my beef with this is pedantic. If I have three entities --

  1. Course
  2. Student
  3. Professor

... I don't necessarily want all the generated code to be mashed under one package. I want to have a base package of com.university and then have a package for each so that I end up with --

  1. com.university.course
  2. com.university.student
  3. com.university.professor

.. just like Liferay does in the core. Like Liferay does. That is what lead me to dig in and find out how I might be able to do this, because I know that Liferay uses service builder to generate it's entities and they're not all stuck under a single package. At the time of this investigation I was using maven. I went wading through the liferay-maven-support source. Low and behold. there is a parameter that you can pass to service builder to tell it where to find the service.xml file -- meaning I don't have to keep one file under /WEB-INF and in fact there is no reason why I can't have several service.xml files (as Liferay does) and pass the file as a parameter. Multiple service.xml files means multiple package declarations, but all within the same project. Note though, that you do have to patch the maven tools to get this to work because there is a bug that (to my knowledge) is not yet fixed -- you can reference it here: https://issues.liferay.com/browse/MAVEN-147?jql=project%20%3D%20MAVEN%20AND%20text%20~%20%22service%20build%20and%22

There is no point is trying to use the feature before you patch it, so let's run through this logically. First we'll fix the maven stuff. Then we'll make a sample project to show how to use it.

 

Patching Maven


We're going to start get getting the maven tools locally. You can clone the repo if you like, but I'm going to just download the source from Github. Your version will of course matter. This link is for 6.2.5 GA6 -- but you can use the tags to pick the version you need: https://github.com/liferay/liferay-maven-support/tree/6.2.5

Again, instead of cloning, I'm going to just download a zip as you can see from the image below.

Once everything is downloaded, unpack the archive to a location. In my case, I have an area of my disk where I keep things that are common across my (Liferay) projects. I use /opt/liferay so I'm going to upack it there so that I end up with /opt/liferay/liferay-maven-support-6.2.5 -- and from here on in, I'll refer to his location as LIFERAY_MAVEN_SUPPORT.

The file we need to edit is a java file that is in this package. Open the file ServiceBuilderMojo.java that is found in LIFERAY_MAVEN_SUPPORT/plugins/liferay-maven-plugin/src/main/java/com/liferay/maven/plugins/. If you are not deeply familiar with Maven, and building your own plugins, using classes that are post fixed with the word *Mojo is the convention. If you take a moment to look through the files in this directory you'll probably find at least a few names that are familair to you from your build process output -- some of the magic is revealed.

The fix is to comment out a line.

1. Open the ServiceBuilderMojo file

2. Go to line number 252

3. Comment out the line you find there so that it looks like this

...
else {
	hbmFileName = webappResourcesDir.concat("/META-INF/portlet-hbm.xml");
	jsonFileName = webappDir.concat("/js/service.js");
	modelHintsFileName = webappResourcesDir.concat("/META-INF/portlet-model-hints.xml");
	ormFileName = webappResourcesDir.concat("/META-INF/portlet-orm.xml");
	//serviceFileName = webappDir.concat("/WEB-INF/service.xml");
	springBaseFileName = webappResourcesDir.concat("/META-INF/base-spring.xml");
	springClusterFileName = webappResourcesDir.concat("/META-INF/cluster-spring.xml");
...	
NOTE: depending on the version you are working with - the line number that you need to comment out might change. The code is the same though so you should be able to just search for the line referenced above. You'll know if you got it right because if you didn't comment out the right line, you'll get an error saying it can't find your service.xml file as you can see in the JIRA issue referenced earlier.

Why do we comment this out. Well, if you used a debugger to step through this code you'll find that when it runs, it does pick up the parameter you pass, but this line overrides the parameter with the default location /WEB-INF/service.xml. And if you don't pass a value? this doesn't break it -- the absence of the paramter assigns the correct value which means bascially this line is unecessary. If you have the time, you should study this file for a few minutes as it can open your eyes to a lot of service builder magic that is not really something that is advertised. You might even find an out of the box way to address something that you are --- ummm "creatively" solving right now :)

4. When you are done, Save the file and close it.

5. Next we're going to build and install the plugins (locally). For me, on the command line I go to LIFERAY_MAVEN_SUPPORT where we see a pom.xml. There I can use the standard maven stuff.

$>mvn clean install

Once it is done, you can validate in your maven local repo that the libraries are there. When you are satisfied that you have patched and installed the plugins, move on to putting them in use.

 

Using Multiple service.xml Files


I'm not going to go through how to create a portlet here. I'm going to assume that you already know how to do that, or that you have a plugin you want to apply this to already. Obviously, it needs to be a service builder plugin type. My structure looks like this --

└── sample-service-builder
    ├── pom.xml
    ├── sample-service-builder-portlet
    │   ├── pom.xml
    │   └── src
    │       └── main
    │           ├── java
    │           ├── resources
    │           │   └── portlet.properties
    │           └── webapp
    │               ├── css
    │               │   └── main.css
    │               ├── icon.png
    │               ├── js
    │               │   └── main.js
    │               ├── view.jsp
    │               └── WEB-INF
    │                   ├── liferay-display.xml
    │                   ├── liferay-plugin-package.properties
    │                   ├── liferay-portlet.xml
    │                   ├── portlet.xml
    │                   ├── service.xml
    │                   └── web.xml
    └── sample-service-builder-portlet-service
        └── pom.xml

1. Let's start by adding the packages that we referenced above. Go to the /sample-service-builder/sample-service-builder-portlet/src/main/java and add the hierarchies so that you have

  1. ../src/main/java/com/university/course
  2. ../src/main/java/com/university/student
  3. ../src/main/java/com/university/professor

2. Next we're going to add a service.xml file to each location. Now I have --

  1. ../src/main/java/com/university/course/service.xml
  2. ../src/main/java/com/university/student/service.xml
  3. ../src/main/java/com/university/professor/service.xml

3. Now put what you like in each of these files. Here is what I put in my course service.xml and I basically used the same structure in the student and professor ones, but changed the package and entity names accordingly. So I am giving the full file for course, but a partial for the others.

 

../course/service.xml

<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_2_0.dtd">

<service-builder package-path="com.university.course">
    <namespace>UNIVERSITY</namespace>
    <entity name="Course" uuid="true" local-service="true" remote-service="false" cache-enabled="true">

        <!-- PK fields -->
        <column name="courseId" type="long" primary="true"/>

        <!-- Group instance -->
        <column name="groupId" type="long"/>

        <!-- Audit fields -->
        <column name="companyId" type="long"/>
        <column name="userId" type="long"/>
        <column name="userName" type="String"/>
        <column name="createDate" type="Date"/>
        <column name="modifiedDate" type="Date"/>

        <!-- Other fields -->
        <column name="name" type="String"/>
    </entity>
</service-builder>

../student/service.xml

<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_2_0.dtd">

<service-builder package-path="com.university.student">
    <namespace>UNIVERSITY</namespace>
    <entity name="Student" uuid="true" local-service="true" remote-service="false" cache-enabled="true">

    ...
</service-builder>

../professor/service.xml

<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 6.2.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_6_2_0.dtd">

<service-builder package-path="com.university.professor">
    <namespace>UNIVERSITY</namespace>
    <entity name="Professor" uuid="true" local-service="true" remote-service="false" cache-enabled="true">

    ...
</service-builder>

4. With this in place, we have all that we need to generate our code. Now, you can run it using the command line, but honestly, I struggle to remember all the commands so I like to add profiles to my pom so that I can just run the standard maven command to trigger service builder. Open the /sample-service-builder/sample-service-builder-portlet/pom.xml

5. After the block outlining the <build> add the following chunk of code.

<profiles>
      <profile>
          <id>build-course</id>
          <activation>
              <property>
                  <name>build-course</name>
              </property>
          </activation>
          <build>
              <plugins>
                  <plugin>
                      <groupId>com.liferay.maven.plugins</groupId>
                      <artifactId>liferay-maven-plugin</artifactId>
                      <version>${liferay.maven.plugin.version}</version>
                      <executions>
                          <execution>
                              <phase>generate-sources</phase>
                              <goals>
                                  <goal>build-service</goal>
                              </goals>
                              <configuration>
                                  <serviceFileName>
                                      ${basedir}/src/main/java/com/university/course/service.xml
                                  </serviceFileName>
                              </configuration>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
          </build>
      </profile>
      <profile>
          <id>build-student</id>
          <activation>
              <property>
                  <name>build-student</name>
              </property>
          </activation>
          <build>
              <plugins>
                  <plugin>
                      <groupId>com.liferay.maven.plugins</groupId>
                      <artifactId>liferay-maven-plugin</artifactId>
                      <version>${liferay.maven.plugin.version}</version>
                      <executions>
                          <execution>
                              <phase>generate-sources</phase>
                              <goals>
                                  <goal>build-service</goal>
                              </goals>
                              <configuration>
                                  <serviceFileName>
                                      ${basedir}/src/main/java/com/university/student/service.xml
                                  </serviceFileName>
                              </configuration>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
          </build>
      </profile>
      <profile>
          <id>build-professor</id>
          <activation>
              <property>
                  <name>build-professor</name>
              </property>
          </activation>
          <build>
              <plugins>
                  <plugin>
                      <groupId>com.liferay.maven.plugins</groupId>
                      <artifactId>liferay-maven-plugin</artifactId>
                      <version>${liferay.maven.plugin.version}</version>
                      <executions>
                          <execution>
                              <phase>generate-sources</phase>
                              <goals>
                                  <goal>build-service</goal>
                              </goals>
                              <configuration>
                                  <serviceFileName>
                                      ${basedir}/src/main/java/com/university/professor/service.xml
                                  </serviceFileName>
                              </configuration>
                          </execution>
                      </executions>
                  </plugin>
              </plugins>
          </build>
      </profile>
</profiles>
NOTE: I also have added properties to the root projects pom specifying the versions of liferay and java that I want to use. If you don't specify the versions of Liferay you will get an error, but that has nothing specifically to do with what we're doing here.

What have we done? Nothing earth shattering. We just added a few profiles, one for each service.xml file. We use a common naming pattern so that build-* where the * we can substitude with course, student and professor. Perhaps this is not the most elegant solution but it works for me and is portable to other team members.

6. Alright, last step. A few command line commands to get the code to be generated.

$>mvn -P build-course generate-sources
$>mvn -P build-student generate-sources
$>mvn -P build-professor generate-sources
		

7. I'm not going to paste the entire tree. But you can see from this snapshot that I have individual packages for my entities.

sample-service-builder-portlet-service
      ├── pom.xml
      └── src
          └── main
              └── java
                  └── com
                      └── university
                          ├── course
                          │   ├── model
                          │   │   ├── CourseClp.java
                          │   │   ├── ...
                          │   │   └── CourseWrapper.java
                          │   ├── NoSuchCourseException.java
                          │   └── service
                          │       ├── ClpSerializer.java
                          │       ├── ...
                          │       ├── messaging
                          │       │   └── ClpMessageListener.java
                          │       └── persistence
                          │           ├── ...
                          │           └── CourseUtil.java
                          ├── professor
                          │   ├── model
                          │   │   ├── ProfessorClp.java
                          │   │   └── ...
                          │   ├── NoSuchProfessorException.java
                          │   └── service
                          │       ├── ClpSerializer.java
                          │       ├── messaging
                          │       │   └── ClpMessageListener.java
                          │       ├── persistence
                          │       │   ├── ...
                          │       │   └── ProfessorUtil.java
                          │       ├── ...
                          │       └── ProfessorLocalServiceWrapper.java
                          └── student
                              ├── model
                              │   ├── StudentClp.java
                              │   ├── ...
                              │   └── StudentWrapper.java
                              ├── NoSuchStudentException.java
                              └── service
                                  ├── ClpSerializer.java
                                  ├── messaging
                                  │   └── ClpMessageListener.java
                                  ├── persistence
                                  │   ├── ...
                                  │   └── StudentUtil.java
                                  ├── ...
                                  └── StudentLocalServiceWrapper.java

			

 

Conclusion


So why does this matter, and why would you bother. Honestly, it probably doesn't matter much, and most people probably wouldn't bother. I do it because I like the separation in the packages but it's almost purely aesthetic. I suppose you could argue that, if you have project with A LOT of entities, that it will build faster since you only build the relevant entities, but I'm not sure that the time savings would be so drastic that it's a reason. If it is, then you probably should take a look at how you project is structured :). In truth I went down this path primarily for curiosity and to learn something new. Having found it, I now basically use this model exclusively, even if I only need one package in my plugin and thought maybe someone else might want to try it as well. Are you going to be crowned the hero of your team? or warrant a little extra on your bonus for using this? probably not.

What about Liferay 7. Well, in truth I haven't tried this on 7 yet. Mostly because of the lack of time but also because the architecture for 7 is such that I find myself using the "plugin per entity" type solution rather than clumping them together. I'm also not sure if/how you would do this with gradle, or ANT for that matter -- but I am willing to bet it is possible. Maybe I'll put it on my list and, if I get that far down my list, update this post with what I find.