This article shows you an easy way to update portal-ext.properties without a restart. You will speed up your development without always requiring a Liferay restart when you change the portal-ext.properties file. This method will not work flawlessly for every property key/value pair and we would never recommend doing this in production, but there are MANY cases where this is sufficient for development.

Use Case

As a Liferay developer, you have likely run into a situation where you need to change some properties in portal-ext.properties to overwrite what you have in portal.properties, add new properties, or just to update existing properties in there. This happens much more often during the development phase. Each time you make a change, you must restart your Liferay server to make your changes available. On the low end of the timescale, each restart takes at least 3 minutes. As your amount of code and number of deployable artifacts increases, the amount of time to restart goes up as well. If you make 10 restarts per day, we’re talking about losing 30 minutes of productivity and that’s a healthy slice of a workday. It would be great if we can stop wasting time for such thing.

Solution Overview

I have come up with a simple solution to solve that problem by implementing a scheduler triggered after every 5 seconds in the Liferay server. This scheduler will look for an updated timestamp on the deployed portal-ext.properties file. If it gets changed, the scheduler will read the file for updated properties and reload the memory with updated values.

Getting started

Firstly we need to create a scheduler. In order to do this, we simply extend BaseSchedulerEntryMessageListener and override doReceive() method as below:

@Component(immediate = true, service = PropertiesUpdateScheduler.class)
public class PropertiesUpdateScheduler extends BaseSchedulerEntryMessageListener {
	@Override
	protected void doReceive(Message message) throws Exception {
	}
}

Secondly, we make it triggered after every 5 seconds by adding some codes to activate() method as follows:

@Activate
@Modified
protected void activate() {
	schedulerEntryImpl.setTrigger(
	TriggerFactoryUtil.createTrigger(getEventListenerClass(), getEventListenerClass(), 5, TimeUnit.SECOND));

_schedulerEngineHelper.register(this, schedulerEntryImpl, DestinationNames.SCHEDULER_DISPATCH);
}

@Reference(unbind = "-")
protected void setSchedulerEngineHelper(SchedulerEngineHelper schedulerEngineHelper) {
	_schedulerEngineHelper = schedulerEngineHelper;
}
private SchedulerEngineHelper _schedulerEngineHelper;

Finally, we add logic to reload updated values of portal-ext.properties in memory. To do this, first, we need to store the last modified timestamp of portal-ext.properties file and maintain the list of properties defined in portal.properties and portal-ext.properties the first time we deploy the scheduler by loading them into maps.

private static final Log LOG = LogFactoryUtil.getLog(PropertiesUpdateScheduler.class);
private static long lastModified = 0;
private static String PORTAL_PROPERTIES = "portal.properties";
private static String PORTAL_EXT_PROPERTIES = "portal-ext.properties";
private static Map<String, String> portalExtPropertiesMap = new HashMap<>();
private static Map<String, String> portalPropertiesMap = new HashMap<>();

private void initMaps() {
if (portalExtPropertiesMap.size() == 0) {
		try {
			InputStream portalExtPropertiesInputStream = getInputStreamByFileName(PORTAL_EXT_PROPERTIES);
			if (portalExtPropertiesInputStream == null) {
				throw new IOException();
			}
			Properties properties = new Properties();
			properties.load(portalExtPropertiesInputStream);
			for (Object obj : properties.keySet()) {
				String key = (String) obj;
				portalExtPropertiesMap.put(key, properties.getProperty(key));
			}
		} catch (IOException | IllegalArgumentException e) {
			LOG.warn("portal-ext.properties file does not exists or it is in invalid format");
		}
	}
	if (portalPropertiesMap.size() == 0) {
		try {
			InputStream portalPropertiesInputStream = getInputStreamByFileName(PORTAL_PROPERTIES);
			if (portalPropertiesInputStream == null) {
				throw new IOException();
			}
			Properties properties = new Properties();
			properties.load(portalPropertiesInputStream);
			for (Object obj : properties.keySet()) {
				String key = (String) obj;
				portalPropertiesMap.put(key, properties.getProperty(key));
			}
		} catch (IOException | IllegalArgumentException e) {
			LOG.error("portal.properties file does not exists or it is in invalid format");
		}
	}
}

The next and final step is to add logic to doReceive() method to reload updated properties values into memory and update the last modified timestamp with the last modified timestamp of the portal-ext.properties file. The key thing is to invoke PropUtil.set(key, value) to update property’s value in memory.

ClassLoader classLoader = VerifyProperties.class.getClassLoader();
URL resource = classLoader.getResource(PORTAL_EXT_PROPERTIES);
// update portal ext properties only if the file exists
if (resource != null) {
	String filePath = resource.getPath();
	File file = new File(filePath);
	if (file.exists()) {
		if (lastModified != file.lastModified()) { // file has changed
			InputStream inputStream = getInputStreamByFileName(PORTAL_EXT_PROPERTIES);
			Properties properties = new Properties();
			properties.load(inputStream);
			List<String> latestKeys = new ArrayList<>();
			// update only changed properties
			for (Object obj : properties.keySet()) {
				String key = (String) obj;
				String value = properties.getProperty(key);
				if (!portalExtPropertiesMap.containsKey(key)) {
					PropsUtil.set(key, value);
					portalExtPropertiesMap.put(key, value);
				} else if (!value.equals(portalExtPropertiesMap.get(key))) {
					PropsUtil.set(key, value);
				}
				latestKeys.add(key);
			}

			// if an overwrite property is deleted, set it back to the
			// value in portal.properties
			// otherwise set it to null
			Iterator<String> iter = portalExtPropertiesMap.keySet().iterator();
			while (iter.hasNext()) {
				String key = iter.next();
				if (!latestKeys.contains(key)) {
					if (portalPropertiesMap.containsKey(key)) {
						PropsUtil.set(key, portalPropertiesMap.get(key));
					} else {
						PropsUtil.set(key, null);
						PropsUtil.getProperties().remove(key);
					}
					iter.remove();
				}
			}
			lastModified = file.lastModified();
			LOG.info("Last modified: " + lastModified);
		}
	}
}
private InputStream getInputStreamByFileName(String fileName) {
	ClassLoader classLoader = VerifyProperties.class.getClassLoader();
	return classLoader.getResourceAsStream(fileName);
}

Everything is ready to go when you build and deploy this scheduler to Liferay server.

Summary

The idea is simple, however, it makes my daily life better and easier. Now I quit restarting the Liferay server and avoid wasting my time for such things. Like I said, it’s not ideal for all cases, and it should not be used in production, but it’s certainly helpful for development. I hope it will bring you the same usefulness.

If you have questions and/or need any help, please engage with us via comments on this blog post, or reach out to us at https://www.xtivia.com/contact/ or [email protected].

Additional Reading

You can check out Portal Propertiess for more information about configuring Liferay.
You can also explore more details on how to create a scheduler in Liferay DXP by checking out How to create scheduler.

Share This