package com.constellio.app.services.extensions.plugins;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.ID_MISMATCH;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_EXISTING_ID;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_ID_FORMAT;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_JAR;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_MANIFEST;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_MIGRATION_SCRIPT;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_START;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.IO_EXCEPTION;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.JAR_NOT_FOUND;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.JAR_NOT_SAVED_CORRECTLY;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.MORE_THAN_ONE_INSTALLABLE_MODULE_PER_JAR;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.NO_ID;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.NO_INSTALLABLE_MODULE_DETECTED_FROM_JAR;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.NO_VERSION;
import static com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus.DISABLED;
import static com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus.ENABLED;
import static com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus.INVALID;
import static com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus.READY_TO_INSTALL;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.xeoh.plugins.base.PluginManager;
import net.xeoh.plugins.base.impl.PluginManagerFactory;
import net.xeoh.plugins.base.util.PluginManagerUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.constellio.app.entities.modules.InstallableModule;
import com.constellio.app.services.extensions.plugins.ConstellioPluginManagerRuntimeException.InvalidId;
import com.constellio.app.services.extensions.plugins.ConstellioPluginManagerRuntimeException.InvalidId.InvalidId_BlankId;
import com.constellio.app.services.extensions.plugins.ConstellioPluginManagerRuntimeException.InvalidId.InvalidId_ExistingId;
import com.constellio.app.services.extensions.plugins.ConstellioPluginManagerRuntimeException.InvalidId.InvalidId_NonAlphaNumeric;
import com.constellio.app.services.extensions.plugins.InvalidPluginJarException.InvalidPluginJarException_InvalidJar;
import com.constellio.app.services.extensions.plugins.InvalidPluginJarException.InvalidPluginJarException_InvalidManifest;
import com.constellio.app.services.extensions.plugins.InvalidPluginJarException.InvalidPluginJarException_NoCode;
import com.constellio.app.services.extensions.plugins.InvalidPluginJarException.InvalidPluginJarException_NoVersion;
import com.constellio.app.services.extensions.plugins.InvalidPluginJarException.InvalidPluginJarException_NonExistingFile;
import com.constellio.app.services.extensions.plugins.PluginServices.PluginsReplacementException;
import com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginInfo;
import com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus;
import com.constellio.app.services.extensions.plugins.utils.PluginManagementUtils;
import com.constellio.data.dao.managers.StatefulService;
import com.constellio.data.io.services.facades.IOServices;
import com.constellio.data.utils.ImpossibleRuntimeException;
import com.constellio.model.conf.FoldersLocator;
import com.constellio.model.conf.FoldersLocatorMode;
import com.constellio.model.entities.modules.ConstellioPlugin;
import com.constellio.model.entities.modules.Module;
public class JSPFConstellioPluginManager implements StatefulService, ConstellioPluginManager {
private static final Logger LOGGER = LogManager.getLogger(JSPFConstellioPluginManager.class);
public static final String PREVIOUS_PLUGINS = "previousPlugins";
private final File pluginsDirectory;
private PluginManager pluginManager;
private final ConstellioPluginConfigurationManager pluginConfigManger;
private Map<String, InstallableModule> registeredModules = new HashMap<>();
private Map<String, InstallableModule> validUploadedPlugins = new HashMap<>();
private IOServices ioServices;
private final File pluginsManagementOnStartupFile;
public JSPFConstellioPluginManager(File pluginsDirectory, File pluginsManagementOnStartupFile, IOServices ioServices,
ConstellioPluginConfigurationManager pluginConfigManger) {
this.pluginConfigManger = pluginConfigManger;
this.pluginsDirectory = pluginsDirectory;
this.pluginsManagementOnStartupFile = pluginsManagementOnStartupFile;
if (pluginsDirectory != null && pluginsDirectory.isDirectory()) {
//FIXME see Cis
File saveOldPluginsDestination = new File(pluginsDirectory, PREVIOUS_PLUGINS);
if (!saveOldPluginsDestination.exists()) {
try {
FileUtils.forceMkdir(saveOldPluginsDestination);
} catch (IOException e) {
LOGGER.error("Error when trying to create old plugins, please replace them manually", e);
throw new RuntimeException(e);
}
}
}
this.ioServices = ioServices;
pluginConfigManger.createConfigFileIfNotExist();
}
public void detectPlugins() {
initialize();
this.pluginManager = PluginManagerFactory.createPluginManager();
if (pluginsDirectory != null && pluginsDirectory.isDirectory()) {
try {
newPluginServices().replaceOldPluginVersionsByNewOnes(pluginsDirectory,
new File(pluginsDirectory, PREVIOUS_PLUGINS));
} catch (PluginsReplacementException e) {
for (String pluginId : e.getPluginsWithReplacementExceptionIds()) {
pluginConfigManger.invalidateModule(pluginId, IO_EXCEPTION, e);
}
}
for (ConstellioPluginInfo pluginInfo : getPlugins(ENABLED, DISABLED, READY_TO_INSTALL)) {
LOGGER.info("Detected plugin : " + pluginInfo.getCode());
installValidPlugin(pluginInfo);
}
handlePluginsDependency();
//Be aware of order : invalid are last plugins to be handled
for (ConstellioPluginInfo pluginInfo : getPlugins(INVALID)) {
installInvalidPlugin(pluginInfo.getCode());
}
}
}
//TODO in future version
private void handlePluginsDependency() {
for (InstallableModule pluginModule : validUploadedPlugins.values()) {
//1. ensure no module depend on invalid module otherwise it is also invalid
List<String> dependencies = pluginModule.getDependencies();
if (dependencies != null) {
for (String dependency : dependencies) {
if (!registeredModules.keySet().contains(dependency)) {
InstallableModule dependOnModule = validUploadedPlugins.get(dependency);
if (dependOnModule == null
|| pluginConfigManger.getPluginInfo(dependency).getPluginStatus() == DISABLED) {
//TODO disable and remove from validUploadedPlugins see avec Cis
}
}
}
}
//2. cyclic dependency is not supported hence save only oldest plugins
}
}
private void installInvalidPlugin(String pluginId) {
PluginServices helperService = newPluginServices();
File pluginJar = helperService.getPluginJar(pluginsDirectory, pluginId);
if (pluginJar == null) {
LOGGER.error("Invalid plugin " + pluginId + " not found in plugins directory");
} else {
try {
pluginManager.addPluginsFrom(pluginJar.toURI());
} catch (Throwable e) {
LOGGER.error("Error when trying to load invalid plugin " + pluginId, e);
}
}
}
void installValidPlugin(ConstellioPluginInfo existingInfo) {
PluginServices helperService = newPluginServices();
String pluginId = existingInfo.getCode();
File pluginJar = helperService.getPluginJar(pluginsDirectory, pluginId);
if (pluginJar == null) {
String error = "Plugin " + pluginId + " not found in plugins directory";
LOGGER.error(error);
pluginConfigManger.invalidateModule(pluginId, JAR_NOT_FOUND, null);
} else {
try {
PluginActivationFailureCause failure = registerPlugin(pluginJar, pluginId);
if (existingInfo.getPluginStatus().equals(READY_TO_INSTALL)) {
LOGGER.info("Plugin " + pluginId + " was ready to installed, now marked as enabled");
pluginConfigManger.markPluginAsEnabled(pluginId);
}
if (failure != null) {
LOGGER.info("Plugin " + pluginId + " failed : " + failure);
pluginConfigManger.invalidateModule(existingInfo.getCode(), failure, null);
}
} catch (Throwable e) {
LOGGER.error("Error when trying to register plugin " + existingInfo.getCode(), e);
pluginConfigManger.invalidateModule(existingInfo.getCode(), JAR_NOT_FOUND, e);
}
}
}
private PluginActivationFailureCause registerPlugin(File pluginJar, String pluginId) {
PluginManagerUtil util = new PluginManagerUtil(pluginManager);
Collection<InstallableModule> pluginsBefore = util.getPlugins(InstallableModule.class);
pluginManager.addPluginsFrom(pluginJar.toURI());
util = new PluginManagerUtil(pluginManager);
Collection<InstallableModule> pluginsAfter = util.getPlugins(InstallableModule.class);
if (pluginsAfter.size() == pluginsBefore.size()) {
return NO_INSTALLABLE_MODULE_DETECTED_FROM_JAR;
} else if (pluginsAfter.size() > pluginsBefore.size() + 1) {
return MORE_THAN_ONE_INSTALLABLE_MODULE_PER_JAR;
}
int pluginsWithPluginIdCount = 0;
InstallableModule newInstallableModule = null;
for (InstallableModule plugin : pluginsAfter) {
if (plugin.getId() != null && plugin.getId().equals(pluginId)) {
newInstallableModule = plugin;
pluginsWithPluginIdCount++;
}
}
if (pluginsWithPluginIdCount != 1) {
return ID_MISMATCH;
} else {
validUploadedPlugins.put(pluginId, newInstallableModule);
}
return null;
}
@Override
public void registerModule(InstallableModule plugin)
throws InvalidId {
if (plugin != null) {
validateId(plugin.getId());
registeredModules.put(plugin.getId(), plugin);
}
}
public void unregisterModule(InstallableModule plugin)
throws InvalidId {
if (plugin != null) {
registeredModules.remove(plugin.getId());
}
}
@Override
public void registerPluginOnlyForTests(InstallableModule plugin)
throws InvalidId {
if (plugin != null) {
validateId(plugin.getId());
this.pluginConfigManger.addOrUpdatePlugin(
new ConstellioPluginInfo().setCode(plugin.getId()).setPluginStatus(ENABLED).setTitle(plugin.getName()));
validUploadedPlugins.put(plugin.getId(), plugin);
}
}
void validateId(String id)
throws InvalidId {
if (StringUtils.isBlank(id)) {
throw new InvalidId_BlankId(id);
}
if (registeredModules.keySet().contains(id) || validUploadedPlugins.containsValue(id)) {
throw new InvalidId_ExistingId(id);
}
Pattern pattern = Pattern.compile("(\\w)*");
Matcher matcher = pattern.matcher(id);
if (!matcher.matches()) {
throw new InvalidId_NonAlphaNumeric(id);
}
}
@Override
public void initialize() {
pluginConfigManger.createConfigFileIfNotExist();
markNewPluginsInNewWarAsInstalled(new FoldersLocator());
}
@Override
public List<InstallableModule> getRegistredModulesAndActivePlugins() {
ensureStarted();
List<InstallableModule> plugins = new ArrayList<>();
plugins.addAll(registeredModules.values());
plugins.addAll(getActivePluginModules());
return plugins;
}
@Override
public List<InstallableModule> getRegisteredModules() {
ensureStarted();
List<InstallableModule> plugins = new ArrayList<>();
plugins.addAll(registeredModules.values());
return plugins;
}
@Override
public List<InstallableModule> getActivePluginModules() {
List<InstallableModule> returnList = new ArrayList<>();
List<String> activePluginModulesIds = pluginConfigManger.getActivePluginsIds();
for (InstallableModule pluginModule : validUploadedPlugins.values()) {
if (activePluginModulesIds.contains(pluginModule.getId())) {
returnList.add(pluginModule);
}
}
return returnList;
}
@Override
public PluginActivationFailureCause prepareInstallablePlugin(File jarFile) {
PluginServices helperService = newPluginServices();
ConstellioPluginInfo newPluginInfo;
try {
newPluginInfo = helperService.extractPluginInfo(jarFile);
validateId(newPluginInfo.getCode());
} catch (InvalidPluginJarException e) {
return getAdequateFailureCause(e);
} catch (InvalidId_BlankId | InvalidId_NonAlphaNumeric e) {
return INVALID_ID_FORMAT;
} catch (InvalidId_ExistingId e3) {
return INVALID_EXISTING_ID;
}
ConstellioPluginInfo existingPluginInfo = pluginConfigManger.getPluginInfo(newPluginInfo.getCode());
PluginActivationFailureCause failure = helperService.validatePlugin(newPluginInfo, existingPluginInfo);
if (failure != null) {
return failure;
}
try {
helperService.saveNewPlugin(pluginsDirectory, jarFile, newPluginInfo.getCode());
pluginConfigManger.installPlugin(newPluginInfo.getCode(), newPluginInfo.getTitle(),
newPluginInfo.getVersion(), newPluginInfo.getRequiredConstellioVersion());
addPluginToManageOnStartupList(newPluginInfo.getCode());
} catch (IOException e) {
LOGGER.error("Exception when saving new plugin", e);
return JAR_NOT_SAVED_CORRECTLY;
}
return null;
}
@Override
public PluginActivationFailureCause prepareInstallablePluginInNextWebapp(File jarFile, File nextWebapp) {
PluginServices helperService = newPluginServices();
ConstellioPluginInfo newPluginInfo;
try {
newPluginInfo = helperService.extractPluginInfo(jarFile);
validateId(newPluginInfo.getCode());
} catch (InvalidPluginJarException e) {
return getAdequateFailureCause(e);
} catch (InvalidId_BlankId | InvalidId_NonAlphaNumeric e) {
return INVALID_ID_FORMAT;
} catch (InvalidId_ExistingId e3) {
return INVALID_EXISTING_ID;
}
ConstellioPluginInfo existingPluginInfo = pluginConfigManger.getPluginInfo(newPluginInfo.getCode());
PluginActivationFailureCause failure = helperService.validatePlugin(newPluginInfo, existingPluginInfo);
if (failure != null) {
return failure;
}
File pluginsDirectory = new File(nextWebapp, "WEB-INF" + File.separator + "plugins");
File libDirectory = new File(nextWebapp, "WEB-INF" + File.separator + "lib");
File jarfileInNextWarPlugins = new File(pluginsDirectory, newPluginInfo.getCode() + ".jar");
File jarfileInNextWarLibs = new File(libDirectory, newPluginInfo.getCode() + ".jar");
jarfileInNextWarPlugins.delete();
jarfileInNextWarLibs.delete();
try {
FileUtils.copyFile(jarFile, jarfileInNextWarPlugins);
FileUtils.moveFile(jarFile, jarfileInNextWarLibs);
} catch (IOException e) {
throw new RuntimeException(e);
}
PluginManagementUtils.markNewPluginsInNewWar(nextWebapp, jarfileInNextWarPlugins.getName());
return null;
}
public void markNewPluginsInNewWarAsInstalled(FoldersLocator foldersLocator) {
File webapp = foldersLocator.getConstellioWebappFolder();
File plugins = foldersLocator.getPluginsJarsFolder();
LOGGER.info("markNewPluginsInNewWarAsInstalled(" + webapp.getAbsolutePath() + ")");
List<String> newPluginsFileNames = PluginManagementUtils.getNewPluginsInNewWar(webapp);
if (!newPluginsFileNames.isEmpty()) {
for (String newPluginFilename : newPluginsFileNames) {
File newPlugin = new File(plugins, newPluginFilename);
PluginServices helperService = newPluginServices();
ConstellioPluginInfo newPluginInfo;
try {
newPluginInfo = helperService.extractPluginInfo(newPlugin);
validateId(newPluginInfo.getCode());
} catch (Exception e) {
throw new ImpossibleRuntimeException(e);
}
LOGGER.info("mark plugin " + newPluginFilename + "' in new war '" + webapp.getAbsolutePath() + "' as installed");
pluginConfigManger.installPlugin(newPluginInfo.getCode(), newPluginInfo.getTitle(),
newPluginInfo.getVersion(), newPluginInfo.getRequiredConstellioVersion());
}
}
PluginManagementUtils.clearNewPluginsInNewWar(webapp);
}
void addPluginToManageOnStartupList(String code) {
PluginManagementUtils utils = new PluginManagementUtils(pluginsDirectory, null, pluginsManagementOnStartupFile);
try {
utils.addPluginToMove(code);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private PluginActivationFailureCause getAdequateFailureCause(InvalidPluginJarException e) {
if (e instanceof InvalidPluginJarException_NonExistingFile) {
return JAR_NOT_FOUND;
} else if (e instanceof InvalidPluginJarException_InvalidManifest) {
return INVALID_MANIFEST;
} else if (e instanceof InvalidPluginJarException_NoVersion) {
return NO_VERSION;
} else if (e instanceof InvalidPluginJarException_NoCode) {
return NO_ID;
} else if (e instanceof InvalidPluginJarException_InvalidJar) {
return INVALID_JAR;
} else {
throw new RuntimeException("Unsupported exception ", e);
}
}
@Override
public void markPluginAsEnabled(String pluginId) {
pluginConfigManger.markPluginAsEnabled(pluginId);
}
@Override
public void markPluginAsDisabled(String pluginId) {
pluginConfigManger.markPluginAsDisabled(pluginId);
}
@Override
public void handleModuleNotStartedCorrectly(Module module, String collection, Throwable throwable) {
if (isPluginModule(module)) {
pluginConfigManger.invalidateModule(module.getId(), INVALID_START, throwable);
} else {
LOGGER.error("module not migrated correctly", throwable);
throw new RuntimeException(throwable);
}
}
@Override
public boolean isPluginModule(Module module) {
return isPluginModule(module.getId());
}
private boolean isPluginModule(String id) {
return !registeredModules.keySet().contains(id);
}
@Override
public void handleModuleNotMigratedCorrectly(String moduleId, String collection, Throwable throwable) {
if (isPluginModule(moduleId)) {
if (throwable != null) {
throwable.printStackTrace();
}
pluginConfigManger.invalidateModule(moduleId, INVALID_MIGRATION_SCRIPT, throwable);
} else {
LOGGER.error("module not migrated correctly", throwable);
throw new RuntimeException(throwable);
}
}
@Override
public List<ConstellioPluginInfo> getPlugins(ConstellioPluginStatus... statuses) {
List<ConstellioPluginInfo> returnList = new ArrayList<>();
for (ConstellioPluginStatus status : statuses) {
returnList.addAll(pluginConfigManger.getPlugins(status));
}
return returnList;
}
@Override
public List<String> getPluginsOfEveryStatus() {
return pluginConfigManger.getAllPluginsCodes();
}
@Override
public boolean isRegistered(String id) {
return registeredModules.keySet().contains(id) || validUploadedPlugins.containsValue(id);
}
@Override
public void copyPluginResourcesToPluginsResourceFolder(String moduleId) {
FoldersLocator foldersLocator = new FoldersLocator();
if (foldersLocator.getFoldersLocatorMode() != FoldersLocatorMode.PROJECT) {
File jar = newPluginServices().getPluginJar(pluginsDirectory, moduleId);
if (jar != null && jar.exists()) {
File resourceFolder = foldersLocator.getPluginsResourcesFolder();
newPluginServices().extractPluginResources(jar, moduleId, resourceFolder);
}
}
}
@Override
public <T> Class<T> getModuleClass(String name)
throws ClassNotFoundException {
for (InstallableModule module : getActivePluginModules()) {
try {
return (Class<T>) module.getClass().getClassLoader().loadClass(name);
} catch (ClassNotFoundException e) {
//OK
}
}
throw new ClassNotFoundException(name);
}
@Override
public void removePlugin(String code) {
this.validUploadedPlugins.remove(code);
this.pluginConfigManger.removePlugin(code);
}
@Override
public void configure() {
if (pluginManager != null) {
pluginManager.getPluginConfiguration().setConfiguration(ConstellioPlugin.class, "singletonInitializeMode", "true");
}
}
private void ensureStarted() {
if (pluginManager == null) {
throw new ConstellioPluginManagerRuntimeException("Cannot use plugin manager until it has been started");
}
}
private PluginServices newPluginServices() {
return new JSPFPluginServices(ioServices);
}
@Override
public void close() {
if (this.pluginManager != null) {
this.pluginManager.shutdown();
this.pluginManager = null;
}
}
}