07 June 2019

Now we focus on converting the service layer. This targets legacy code that doesn’t meet the requirements as discussed in Liferay From 6.2 to 7.1: The Model is the key to Services, Security and Search. Before we get started, lets discuss the two different types of services. There are local services and remote services. Local services are designed to have access to the database, provide little security and are only used in the Java code. These types of interfaces and classes are easily recognizable as they are named LocalService.java and LocalServiceImpl.java. Remote services are designed to access local services, have security in place and can be accessed two ways. It can be accessed via Java code and Web Services. These classes are named Service.java and ServiceImpl.java. We will focus on local services. Remote services will bed addressed, in a following article.

Converting Liferay 6.2 local services can be broken down into the following steps.

My old service package path was mil.army.hrc.ikrome.feeds.service. For the new one I went with mil.army.usaac.ikrome.allfeeds. This is also the package I will use for AllFeeds-Web. Note: Changing the package of the models in the service layer led to extra code and difficulties. Namely entries in the ClassName_ and Counter tables.

Now we need to find the servlet context and schema version for our service. In your Liferay deployment go to the Tomcat webapps directory for Liferay 6.2 and find the servlet context. Ours is allFeeds-portlet. We now connect to the database and run query as shown below. Record the schemaVersion and servletContextName. If no results are returned, record 0.0.1 for the schemaVersion and the servlet context found on the file system.

Database Query for Info

select * from Release_ where servletContextName = 'allFeeds-portlet'

servletContextName | schemaVersion
-------------------|---------------
allFeeds-portlet   | 1

To gather the model names we will need to look in the 6.2 service project. In this case it would be the allFeeds/allFeeds-portlet-service/src/main/java/. In subfolders will be the models from the old project. There will be subfolders matching the package-path in the service.xml. In this case our models are mil.army.hrc.ikrome.feeds.service.model.Location and mil.army.hrc.ikrome.feeds.service.model.Newsfeed.

File -> New -> Liferay Module Project. Enter the new module name “AllFeeds-Service”. Verify the location is correct for your modules.Select build type Maven and Liferay Version 7.1. For project template we will choose service-builder. Click Next and enter your package path. Click the Finish button.

Next we edit the pom.xml for AllFeeds-Service-service. Change the buildNumberIncrement to false and add a version number. You will also need to update the service.properties file. Also we have added additional relative paths to mergeModelHintsConfigs. This is so any changes manually made to portlet-model-hints.xml will be preserved even if we run the service builder build from the first two parent directories. The relative directory list should be comma separated and must be all on one line.

modules/AllFeeds-Service/AllFeeds-Service-service/pom.xml

...Stuff Omitted
<build>
	<plugins>
		<plugin>
			<groupId>com.liferay</groupId>
			<artifactId>com.liferay.portal.tools.service.builder</artifactId>
			<version>1.0.248</version>
			<configuration>
				<apiDirName>../AllFeeds-Service-api/src/main/java</apiDirName>
				<autoNamespaceTables>true</autoNamespaceTables>
				<buildNumberIncrement>false</buildNumberIncrement>
				<buildNumber>300000</buildNumber>
				<hbmFileName>src/main/resources/META-INF/module-hbm.xml</hbmFileName>
				<implDirName>src/main/java</implDirName>
				<mergeModelHintsConfigs>src/main/resources/META-INF/portlet-model-hints.xml,AllFeeds-Service/AllFeeds-Service-service/src/main/resources/META-INF/portlet-model-hints.xml,AllFeeds-Service-service/src/main/resources/META-INF/portlet-model-hints.xml</mergeModelHintsConfigs>
				<modelHintsFileName>src/main/resources/META-INF/portlet-model-hints.xml</modelHintsFileName>
				<osgiModule>true</osgiModule>
				<propsUtil>mil.army.usaac.ikrome.allfeeds.service.util.ServiceProps</propsUtil>
				<resourcesDirName>src/main/resources</resourcesDirName>
				<springFileName>src/main/resources/META-INF/spring/module-spring.xml</springFileName>
				<sqlDirName>src/main/resources/META-INF/sql</sqlDirName>
				<sqlFileName>tables.sql</sqlFileName>
			</configuration>
		</plugin>
	</plugins>
</build>
...Stuff Omitted

We can now copy over information from our old service.xml to our new service.xml. Make sure that our new package path is used. The majority of entity definitions can be copied over with no changes.

modules/AllFeeds-Service/AllFeeds-Service-service/service.xml

<?xml version="1.0"?>
<!DOCTYPE service-builder PUBLIC "-//Liferay//DTD Service Builder 7.0.0//EN" "http://www.liferay.com/dtd/liferay-service-builder_7_0_0.dtd">

<service-builder package-path="mil.army.usaac.ikrome.allfeeds">
	<namespace>newsfeed</namespace>
	 <entity name="Newsfeed" local-service="true" remote-service="true">
        <column name="NewsfeedKey" type="String" primary="true"></column>
        <column name="NewsfeedName" type="String"></column>
        <column name="NewsfeedValue" type="String" ></column>
    </entity>
    
    
    <entity name="Location" local-service="true" remote-service="true">
    	<column name="locationId" type="long" primary="true"></column>
    	<column name="companyId" type="long"></column>
    	<column name="groupId" type="long"></column>    	
    	<column name="zip" type="String"></column>    	
    	<column name="name" type="String"></column>
    	
    	<finder return-type="Collection" name="GroupId">
    		<finder-column name="groupId"></finder-column>
    	</finder>
    </entity>
</service-builder>

Now we want to run our service builder using maven so that code will be generated in AllFeeds-Service-Api and AllFeeds-Service-service.

modules/AllFeeds-Service/AllFeeds-Service-service/

$ mvn service-builder:build

The generated code in AllFeeds-Service-Api will likely remain untouched. As shown in Part 1 the cutomizable impl files will be in AllFeeds-Service-service. Copy the code from 6.2 and modify as needed for 7.1. Keep in mind that your import statements will be different and that your IDE may insert the wrong ones as you copy from 6.2 to 7.1. Most likely the only differences in 7.1 will be the import statements.

Now we need to add the bundle activator class. First we need to add dependencies to our pom.xml.

AllFeeds-Service/AllFeeds-Service-service/pom.xml

...Stuff omitted
<dependencies>
...Stuff omitted
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>org.osgi.core</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.osgi</groupId>
      <artifactId>osgi.cmpn</artifactId>
      <version>6.0.0</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>com.liferay</groupId>
      <artifactId>com.liferay.portal.upgrade.api</artifactId>
      <version>2.0.3</version>
      <scope>provided</scope>
    </dependency>
</dependencies>

We will need the servlet context and package path from earlier. The servlet context will be your old symbolic name and the package path appended with “.service” will be your new symbolic name. The namespace will be the namespace from our service.xml. The class should be placed in a package that is the package path appended with “.activator”.

AllFeedsServiceBundleActivator.java

package mil.army.usaac.ikrome.allfeeds.activator;

import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.module.framework.ModuleServiceLifecycle;
import com.liferay.portal.kernel.upgrade.UpgradeException;
import com.liferay.portal.kernel.util.StringBundler;
import com.liferay.portal.upgrade.release.BaseUpgradeServiceModuleRelease;

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Filter;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;

public AllFeedsServiceBundleActivatorclass  implements BundleActivator{
	
	private ServiceTracker<Object, Object> serviceTracker;
	
	@Override
	public void start(BundleContext bundleContext) throws Exception {
		
		Filter filter = bundleContext.createFilter(
		        StringBundler.concat(
		            "(&(objectClass=", ModuleServiceLifecycle.class.getName(), ")",
		            ModuleServiceLifecycle.DATABASE_INITIALIZED, ")"));
		
		serviceTracker = new ServiceTracker<Object, Object>(bundleContext, filter, null) {
			
			@Override
	         public Object addingService(
	          ServiceReference<Object> serviceReference) {
				 
				 try {
		              BaseUpgradeServiceModuleRelease
		                 upgradeServiceModuleRelease =
		                   new BaseUpgradeServiceModuleRelease() {

		                    @Override
		                    protected String getNamespace() {
		                        return "newsfeed";
		                    }

		                    @Override
		                    protected String getNewBundleSymbolicName() {
		                        return "mil.army.usaac.ikrome.allfeeds.service";
		                    }

		                    @Override
		                    protected String getOldBundleSymbolicName() {
		                        return "allFeeds-portlet";
		                    }
		                                   

		                   };
		                   
		              upgradeServiceModuleRelease.upgrade();

		              return null;
		          }
		          catch (UpgradeException ue) {
		              throw new RuntimeException(ue);
		          }
			}
		};
		serviceTracker.open();
	}
	
	@Override
	public void stop(BundleContext context) throws Exception {
		serviceTracker.close();		
	}
	
}

Once our bundle activator class is complete we need to add a reference to it in our bnd.bnd file. Add an entry for Bundle-Activator.

modules/AllFeeds-Service/AllFeeds-Service-service/bnd.bnd

Bundle-Name: AllFeeds-Service-service
Bundle-SymbolicName: mil.army.usaac.ikrome.allfeeds.service
Bundle-Version: 1.0.0
Bundle-Activator:  mil.army.usaac.ikrome.allfeeds.activator.AllFeedsServiceBundleActivator
Liferay-Require-SchemaVersion: 1.0.0
Liferay-Service: true
-includeresource: META-INF/service.xml=service.xml
-liferay-service-xml: META-INF/service.xml
-plugin.service: com.liferay.ant.bnd.service.ServiceAnalyzerPlugin
-plugin.spring: com.liferay.ant.bnd.spring.SpringDependencyAnalyzerPlugin

Adding the upgrade step class is next. Remember my my decision to change the package path? The Counter and ClassName_ upgrade are only necessary because of that decision. The counter upgrade is only applicable for legacy tables with an int or long as a primary key. AllFeeds has one table that meets this criteria. This updates the counter service that produces the primary key values. Next we will update the models names in for class name service. AllFeeds has two tables and both should be updated. First we will need to find the full package path for our model from the 6.2 service builder project. This is mil.army.hrc.ikrome.feeds.service.model.Location and mil.army.hrc.ikrome.feeds.service.model.Newsfeed. The upgrade step code should be placed in the package path appended with “.upgrade”.

Note: The most reliable way to find the old model name is to query the Counter and ClassName_ tables. You can also go into the code in the old 6.2 service project.

AllFeedsUpgradeStep.java

package mil.army.usaac.ikrome.allfeeds.upgrade;

import com.liferay.counter.kernel.model.Counter;
import com.liferay.counter.kernel.service.CounterLocalService;
import com.liferay.portal.kernel.dao.jdbc.DataAccess;
import com.liferay.portal.kernel.exception.PortalException;
import com.liferay.portal.kernel.exception.SystemException;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.model.ClassName;
import com.liferay.portal.kernel.service.ClassNameLocalService;
import com.liferay.portal.kernel.upgrade.UpgradeProcess;

import java.io.IOException;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import mil.army.usaac.ikrome.allfeeds.model.Location;
import mil.army.usaac.ikrome.allfeeds.model.Newsfeed;


public class AllFeedsUpgradeStep extends UpgradeProcess{
	private static Log LOG = LogFactoryUtil.getLog(AllFeedsUpgradeStep.class);
	
	private static final String OLD_LOCATION_MODEL_NAME = "mil.army.hrc.ikrome.feeds.service.model.Location";
	private static final String OLD_NEWFEED_MODEL_NAME = "mil.army.hrc.ikrome.feeds.service.model.Newsfeed";

	private volatile CounterLocalService counterLocalService;
	private volatile ClassNameLocalService classNameLocalService;

	public AllFeedsUpgradeStep(
			CounterLocalService counterLocalService,
			ClassNameLocalService classNameLocalService) {		
		
		this.counterLocalService = Optional.of(counterLocalService).orElseThrow(IllegalArgumentException::new);
		this.classNameLocalService = Optional.of(classNameLocalService).orElseThrow(IllegalArgumentException::new);
		
	}
	
	@Override
	protected void doUpgrade() throws Exception {
		upgradeCounter(OLD_LOCATION_MODEL_NAME, Location.class.getName());
		
		upgradeClassName(OLD_LOCATION_MODEL_NAME, Location.class.getName());
		upgradeClassName(OLD_NEWFEED_MODEL_NAME, Newsfeed.class.getName());
	}
	
	private void upgradeCounter(String oldModelName, String newModelName) {
			Counter oldCounter = counterLocalService.fetchCounter(oldModelName);
			if (oldCounter != null) {
				setNewCounter(newModelName, oldCounter.getCurrentId());
				deleteCounter(oldCounter);
			}
	}	

	private void setNewCounter(String newModelName, long currentId) {
		Counter newCounter = counterLocalService.fetchCounter(newModelName);
		if(newCounter != null) {
			newCounter.setCurrentId(currentId);
			counterLocalService.updateCounter(newCounter);
		}
	}
	
	private void deleteCounter(Counter oldCounter) {
			counterLocalService.deleteCounter(oldCounter);
  }	
	
	protected void upgradeClassName(String oldName, String newName) {

			ClassName oldClassName = classNameLocalService.fetchClassName(oldName);			
			if (oldClassName != null) {
        deleteClassName(newName);
				oldClassName.setClassName(newName);
        classNameLocalService.updateClassName(oldClassName);
			}
	}
	
	protected void deleteClassName(String name) {
		ClassName className = classNameLocalService.fetchClassName(name);			
		if(className != null) {
			classNameLocalService.deleteClassName(className);
		}
	}	
}

I left the upgrade step code, above, as is to illustrate the process. In practice it should, and has, been abstracted out to reusable classes.

Next is our upgrade class. This class will go into the same package as the previous class and will use the schema version we acquired earlier. It will also use the previous class. If you didn’t need the previous class you can replace it with the DummyUpgradeStep class.

AllFeedsUpgrade.java

package mil.army.usaac.ikrome.allfeeds.upgrade;

import com.liferay.counter.kernel.service.CounterLocalService;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import com.liferay.portal.kernel.service.ClassNameLocalService;
import com.liferay.portal.upgrade.registry.UpgradeStepRegistrator;

import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;

@Component(immediate = false, service = UpgradeStepRegistrator.class)
public class AllFeedsUpgrade implements UpgradeStepRegistrator{
	
	private static Log LOG = LogFactoryUtil.getLog(AllFeedsUpgrade.class);
	
	@Reference
	private volatile CounterLocalService counterLocalService;
	
	@Reference
	private volatile ClassNameLocalService classNameLocalService;
	
	@Override
	public void register(Registry registry) {
		LOG.debug("AllFeedsUpgrade register method called.");
		registry.register(
	             "1", "1.0.0",
	            new AllFeedsUpgradeStep(counterLocalService, classNameLocalService));		
	}

}

Finally we copy over the model hints for our service. The legacy copy will be in a similar location under allfeeds-portlet.

AllFeeds-Service/AllFeeds-Service-service/src/main/resources/META-INF/portlet-model-hints.xml

<?xml version="1.0"?>

<model-hints>
	<model name="mil.army.usaac.ikrome.allfeeds.model.Location">
		<field name="locationId" type="long" />
		<field name="companyId" type="long" />
		<field name="groupId" type="long" />
		<field name="zip" type="String" />
		<field name="name" type="String" />
	</model>
	<model name="mil.army.usaac.ikrome.allfeeds.model.Newsfeed">
		<field name="NewsfeedKey" type="String" />
		<field name="NewsfeedName" type="String" />
		<field name="NewsfeedValue" type="String">
			<hint name="max-length">255</hint>
		</field>
	</model>
</model-hints>

Less Is More ~ Older posts are available in the archive.