package com.constellio.app.services.extensions.plugins;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.CANNOT_INSTALL_OLDER_VERSION;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_EXISTING_ID;
import static com.constellio.app.services.extensions.plugins.PluginActivationFailureCause.INVALID_VERSION;
import static com.constellio.app.services.extensions.plugins.pluginInfo.ConstellioPluginStatus.ENABLED;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
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.pluginInfo.ConstellioPluginInfo;
import com.constellio.app.services.migrations.VersionsComparator;
import com.constellio.data.io.services.facades.IOServices;
import com.constellio.data.io.services.zip.ZipService;
import com.constellio.data.io.services.zip.ZipServiceException;
public class JSPFPluginServices implements PluginServices {
private static final Logger LOGGER = LogManager.getLogger(JSPFPluginServices.class);
public static final String NEW_JAR_EXTENSION = "jar.new";
private static final String CODE_ATTRIBUTE_NAME = "code";
private static final String VERSION_ATTRIBUTE_NAME = "version";
private static final String IMPLEMENTATION_TITLE = "Implementation-Title";
private static final String REQUIRED_CONSTELLIO_VERSION_ATTRIBUTE_NAME = "Required-Constellio-Version";
private static final String REQUIRED_CONSTELLIO_VERSION_ATTRIBUTE_NAME_WITH_TYPO = "Requiered-Constellio-Version";
private static final String REQUIRED_CONSTELLIO_VERSION_NULL_VALUE = "null";
private static final String TEMP_UNZIP_FOLDER_FOR_I18N_EXTRACTION_RESOURCES = "JSPFPluginServices-tempUnzipFolderForI18nExtraction";
private IOServices ioServices;
private ZipService zipService;
public JSPFPluginServices(IOServices ioServices) {
this.ioServices = ioServices;
this.zipService = new ZipService(ioServices);
}
@Override
public ConstellioPluginInfo extractPluginInfo(File pluginJar)
throws InvalidPluginJarException {
InputStream stream = null;
JarInputStream jarStream = null;
try {
stream = new FileInputStream(pluginJar);
jarStream = new JarInputStream(stream);
Manifest mf = jarStream.getManifest();
if (mf == null) {
throw new InvalidPluginJarException.InvalidPluginJarException_InvalidManifest();
} else {
return extractPluginInfoFromManifest(mf.getMainAttributes());
}
} catch (IOException e) {
throw new InvalidPluginJarException.InvalidPluginJarException_InvalidJar(e);
} finally {
IOUtils.closeQuietly(stream);
IOUtils.closeQuietly(jarStream);
}
}
private ConstellioPluginInfo extractPluginInfoFromManifest(Attributes attributes)
throws InvalidPluginJarException {
String code = null, requiredConstellioVersion = null, version = null, title = null;
boolean codeFound = false, versionFound = false, requiredConstellioVersionFound = false, titleFound = false;
for (Entry<Object, Object> att : attributes.entrySet()) {
String key = att.getKey().toString();
if (key.equalsIgnoreCase(CODE_ATTRIBUTE_NAME)) {
codeFound = true;
if (att.getValue() != null && StringUtils.isNotBlank(att.getValue().toString())) {
code = att.getValue().toString();
}
}
if (key.equalsIgnoreCase(IMPLEMENTATION_TITLE)) {
titleFound = true;
if (att.getValue() != null && StringUtils.isNotBlank(att.getValue().toString())) {
title = att.getValue().toString();
}
}
if (key.equalsIgnoreCase(VERSION_ATTRIBUTE_NAME)) {
versionFound = true;
if (att.getValue() != null && StringUtils.isNotBlank(att.getValue().toString())) {
version = att.getValue().toString();
}
}
if (key.equalsIgnoreCase(REQUIRED_CONSTELLIO_VERSION_ATTRIBUTE_NAME) ||
key.equalsIgnoreCase(REQUIRED_CONSTELLIO_VERSION_ATTRIBUTE_NAME_WITH_TYPO)) {
requiredConstellioVersionFound = true;
if (att.getValue() != null && StringUtils.isNotBlank(att.getValue().toString())) {
requiredConstellioVersion = att.getValue().toString();
}
}
if (codeFound && versionFound && requiredConstellioVersionFound && titleFound) {
break;
}
}
if (!codeFound) {
throw new InvalidPluginJarException_NoCode();
}
if (!versionFound) {
throw new InvalidPluginJarException_NoVersion();
}
if (StringUtils.isBlank(requiredConstellioVersion)) {
requiredConstellioVersion = "";
} else if (requiredConstellioVersion.equalsIgnoreCase(REQUIRED_CONSTELLIO_VERSION_NULL_VALUE)) {
requiredConstellioVersion = "";
}
if (title == null) {
title = code;
}
return new ConstellioPluginInfo().setCode(code).setTitle(title).setRequiredConstellioVersion(requiredConstellioVersion)
.setVersion(version);
}
@Override
public PluginActivationFailureCause validatePlugin(ConstellioPluginInfo newPluginInfo,
ConstellioPluginInfo previousPluginInfo) {
if (StringUtils.isBlank(newPluginInfo.getCode())) {
return INVALID_EXISTING_ID;
}
String version = newPluginInfo.getVersion();
String previousVersion = null;
if (previousPluginInfo != null && previousPluginInfo.getPluginStatus() == ENABLED) {
previousVersion = previousPluginInfo.getVersion();
}
return validateVersions(version, previousVersion);
}
private PluginActivationFailureCause validateVersions(String version, String oldVersion) {
if (StringUtils.isBlank(version)) {
return INVALID_VERSION;
}
Pattern pattern = Pattern.compile("^(\\d+\\.)?(\\d+\\.)?(\\d+)$");
Matcher matcher = pattern.matcher(version);
if (matcher.matches()) {
if (oldVersion != null &&
VersionsComparator.isFirstVersionBeforeSecond(version, oldVersion)) {
return CANNOT_INSTALL_OLDER_VERSION;
}
} else {
return INVALID_VERSION;
}
return null;
}
public void saveNewPlugin(File pluginsDirectory, File newPluginFile, String pluginCode)
throws IOException {
FileUtils.copyFile(newPluginFile, new File(pluginsDirectory, pluginCode + "." + NEW_JAR_EXTENSION));
}
@Override
public void replaceOldPluginVersionsByNewOnes(File pluginsDirectory, File oldVersionsDestinationDirectory)
throws PluginsReplacementException {
List<String> pluginsWithReplacementException = new ArrayList<>();
for (File newJarVersionFile : FileUtils.listFiles(pluginsDirectory, new String[] { NEW_JAR_EXTENSION }, false)) {
String newVersionFilePath = newJarVersionFile.getPath();
String previousVersionFilePath = newVersionFilePath.substring(0, newVersionFilePath.length() - 4);
File previousVersionFile = new File(previousVersionFilePath);
File oldVersionFile = new File(oldVersionsDestinationDirectory, previousVersionFile.getName());
FileUtils.deleteQuietly(oldVersionFile);
try {
if (previousVersionFile.exists()) {
FileUtils.moveFile(previousVersionFile, oldVersionFile);
FileUtils.deleteQuietly(previousVersionFile);
}
FileUtils.moveFile(newJarVersionFile, previousVersionFile);
} catch (IOException e) {
String pluginId = StringUtils.substringBeforeLast(newJarVersionFile.getName(), "." + NEW_JAR_EXTENSION);
LOGGER.error("Error when trying to replace old plugin " + pluginId, e);
pluginsWithReplacementException.add(pluginId);
}
}
if (!pluginsWithReplacementException.isEmpty()) {
throw new PluginsReplacementException(pluginsWithReplacementException);
}
}
@Override
public File getPluginJar(File pluginsDirectory, String pluginId) {
File pluginFie = new File(pluginsDirectory, pluginId + ".jar");
if (pluginFie.exists()) {
return pluginFie;
}
return null;
}
@Override
public void extractPluginResources(File jar, String pluginId, File pluginsResources) {
File tempUnzipFolder = ioServices.newTemporaryFolder(TEMP_UNZIP_FOLDER_FOR_I18N_EXTRACTION_RESOURCES);
try {
zipService.unzip(jar, tempUnzipFolder);
File resourcesFolderInJar = new File(tempUnzipFolder, pluginId);
File deployedPluginResources = new File(pluginsResources, pluginId);
if (resourcesFolderInJar.exists()) {
ioServices.deleteDirectory(deployedPluginResources);
//deployedPluginResources.mkdirs();
ioServices.copyDirectory(resourcesFolderInJar, deployedPluginResources);
}
} catch (ZipServiceException | IOException e) {
throw new RuntimeException(e);
} finally {
ioServices.deleteQuietly(tempUnzipFolder);
}
}
}