FirstSpirit modules - Visibility control using project-apps

Published on 21 Aug 2019

Whenever extensions for the ContentCreator are developed in a FirstSpirit project, a separate web application is usually also delivered for each project. As the number of projects increases, this solution becomes less and less efficient and requires significantly more resources from the application server. Most of the time a better approach is to operate all projects with a single central WebApp and only activate the required functions for individual projects when required. We will show you how!

Peter Zeidler

Research and Development

Solution

To achieve this behavior, we need a so-called project app in our module. If this is installed in a project, the functionalities of the activated module are made available for this project and thus act as a feature toggle. In addition, the project app provides us with a storage location that we can access from all other components.

Implementation

For the complete implementation we only need a "ProjectApp", a few helper methods and various plugins, which we can use as test objects.

First we need an empty class of type "ProjectApp", which has to be installed in the project to activate the extension.

package de.aboutcontent.blogpost.projectapp;
import de.espirit.firstspirit.module.ProjectApp;
import de.espirit.firstspirit.module.ProjectEnvironment;
import de.espirit.firstspirit.module.descriptor.ProjectAppDescriptor;
public class MyProjectApp implements ProjectApp {
	@Override
	public void init(ProjectAppDescriptor projectAppDescriptor, ProjectEnvironment projectEnvironment) {}
	@Override
	public void installed() {}
	@Override
	public void uninstalling() {}
	@Override
	public void updated(String oldVersion) {}
}

To make the created classes visible we need a new entry in our module.xml file.

<project-app>
	<name>myProjectApp</name>
	<class>de.aboutcontent.blogpost.projectapp.MyProjectApp</class>
</project-app>

Check for activation

To check whether our ProjectApp is installed in the current project, we extend our class with a method that uses the ModuleAdminAgent to figure out in which project the specified module component is installed.

private static boolean isInstalled(ModuleAdminAgent moduleAdminAgent, String moduleName, String componentName, long projectId) {
	Collection<Project> projects = moduleAdminAgent.getProjectAppUsages(moduleName, componentName);
	for (Project project : projects) {
		if (projectId != project.getId()) {
			continue;
		}
		return true;
	}
	return false;
}

This approach would already be sufficient for our application. In order to be able to use the whole however somewhat more comfortable, we add another method, which determines the module and component name based on the class that has been provided.

public static boolean isInstalled(SpecialistsBroker broker, Class<? extends ProjectApp> projectAppClass) {
	ModuleAdminAgent moduleAdminAgent = broker.requireSpecialist(ModuleAdminAgent.TYPE);
	Collection<ModuleDescriptor> modules = moduleAdminAgent.getModules();
	for (ModuleDescriptor moduleDescriptor : modules) {
		ComponentDescriptor[] components = moduleDescriptor.getComponents();
		for (ComponentDescriptor componentDescriptor : components) {
			if (!projectAppClass.getCanonicalName().equals(componentDescriptor.getComponentClass())) {
				continue;
			}
			if (componentDescriptor.getType() != ComponentDescriptor.Type.PROJECTAPP) {
				throw new RuntimeException(String.format("found wrong component type [expected: %s, actual: %s]", ComponentDescriptor.Type.PROJECTAPP.name(), componentDescriptor.getType().name()));
			}
			long projectId = broker.requireSpecialist(ProjectAgent.TYPE).getId();
			return isInstalled(moduleAdminAgent, moduleDescriptor.getName(), componentDescriptor.getName(), projectId);
		}
	}
	throw new RuntimeException(String.format("component or class not found in any module [class: %s]", projectAppClass.getCanonicalName()));
}

With the help of these two methods, we have laid the foundation to be able to check whether the ProjectApp is installed in the current project. Now we can look at its use in a plugin where its visibility can be controlled within FirstSpirit.

Usage

The first thing we'll look at is the usage in a ToolbarPlugin for the ContentCreator. For this we first need a WebApp, which provides our implementations in the ContentCreator.

<web-app scopes="Global,Project">
	<name>myProjectAppWebApp</name> 
	<description>WebApp zum Bereitstellen der Klassen im ContentCreator</description>
	<web-resources>
		${module.resources.global.legacy.web}
	</web-resources>
	<web-xml>web/web.xml</web-xml>
</web-app>

In the actual plugin, the method isVisible controls whether the plugin is displayed or not. So that the ProjectApp is not checked each time the plugin is called, this is only done once during the instantiation of the plugin. In our example, the instantiation of the plugin corresponds to the start of the client.

package de.aboutcontent.blogpost.projectapp;
import java.util.Collection;
import java.util.Collections;
import de.espirit.firstspirit.access.BaseContext;
import de.espirit.firstspirit.agency.OperationAgent;
import de.espirit.firstspirit.client.plugin.toolbar.ToolbarContext;
import de.espirit.firstspirit.ui.operations.RequestOperation;
import de.espirit.firstspirit.webedit.plugin.WebeditToolbarActionsItemsPlugin;
import de.espirit.firstspirit.webedit.plugin.toolbar.ExecutableToolbarActionsItem;
import de.espirit.firstspirit.webedit.plugin.toolbar.WebeditToolbarItem;
public class MyWebeditToolbarActionsItemsPlugin implements WebeditToolbarActionsItemsPlugin {
	public boolean isInstalled = false;
	@Override
	public void setUp(BaseContext context) {
		isInstalled = MyProjectApp.isInstalled(context, MyProjectApp.class);
	}
	@Override
	public void tearDown() {}
	@Override
	public Collection<? extends WebeditToolbarItem> getItems() {
		return Collections.singleton(new WebeditToolbarItemImplementation());
	}
	public final class WebeditToolbarItemImplementation implements ExecutableToolbarActionsItem {
		@Override
		public boolean isVisible(ToolbarContext toolbarContext) {
			return isInstalled;
		}
		@Override
		public boolean isEnabled(ToolbarContext toolbarContext) {
			return true;
		}
		@Override
		public void execute(ToolbarContext toolbarContext) {
			RequestOperation requestOperation = toolbarContext.requireSpecialist(OperationAgent.TYPE).getOperation(RequestOperation.TYPE);
			requestOperation.perform("Die ProjectApp wurde in diesem Projekt installiert");
		}
		@Override
		public String getIconPath(ToolbarContext toolbarContext) {
			return null;
		}
		@Override
		public String getLabel(ToolbarContext toolbarContext) {
			return "MyWebeditToolbarActionsItemsPlugin";
		}
	}
}

The infrastructure created this way can of course not only be used in the ContentCreator but are also available to us in the SiteArchitect.

Using it in other plugins

Similarly, we can use the functionality in plugins that have no visibility control and are always executed as soon as they are installed on the server. In the next example, let's look at the use in an UploadHook. Here we terminate the execution before our actual business logic.

package de.aboutcontent.blogpost.projectapp;
import java.io.IOException;
import java.io.InputStream;
import de.espirit.firstspirit.access.BaseContext;
import de.espirit.firstspirit.access.project.Resolution;
import de.espirit.firstspirit.access.store.mediastore.File;
import de.espirit.firstspirit.access.store.mediastore.Media;
import de.espirit.firstspirit.access.store.mediastore.MediaElement;
import de.espirit.firstspirit.access.store.mediastore.Picture;
import de.espirit.firstspirit.access.store.mediastore.UploadRejectedException;
import de.espirit.firstspirit.service.mediamanagement.UploadHook;
public class MyUploadHook implements UploadHook {
	@Override
	public void postProcess(BaseContext context, Media media, File file, long length) {
		if (!MyProjectApp.isInstalled(context, MyProjectApp.class)) {
			return;
		}
		// do something
	}
	@Override
	public void postProcess(BaseContext context, Media media, Picture picture, long length) {
		if (!MyProjectApp.isInstalled(context, MyProjectApp.class)) {
			return;
		}
		// do something
	}
	@Override
	public void preProcess(BaseContext context, Media media, File file, InputStream inputStream, long length)
			throws UploadRejectedException, IOException {
		if (!MyProjectApp.isInstalled(context, MyProjectApp.class)) {
			return;
		}
		// do something
	}
	@Override
	public void preProcess(BaseContext context, Media media, Picture picture, Resolution reolution, InputStream inputStream, long length)
			throws UploadRejectedException, IOException {
		if (!MyProjectApp.isInstalled(context, MyProjectApp.class)) {
			return;
		}
		// do something
	}
	@Override
	public void uploadAborted(BaseContext context, Media media, MediaElement mediaElement) {
		if (!MyProjectApp.isInstalled(context, MyProjectApp.class)) {
			return;
		}
		// do something
	}
}

Summary

Since a ProjectApp already exists in the module, it can be used without additional effort, for example to provide a central configuration of the project, without it being visible to every user. In addition, this would be transported with every export and import of the project.
The sources of this module are freely available and can be downloaded via Maven. To build the module, however, the tools provided by e-Spirit are required, which were described in our blog entry Getting Started.
If you have further questions about the development of extensions or if you need our support for your project, do not hesitate to contact us!

facebook icon twitter icon xing icon linkedin icon
© 2019 aboutcontent GmbH