/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.plugin.infra.listeners;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import com.thoughtworks.go.util.SystemEnvironment;
import com.thoughtworks.go.util.ZipUtil;
import com.thoughtworks.go.plugin.api.info.PluginDescriptorAware;
import com.thoughtworks.go.plugin.infra.Action;
import com.thoughtworks.go.plugin.infra.ExceptionHandler;
import com.thoughtworks.go.plugin.infra.GoPluginOSGiFramework;
import com.thoughtworks.go.plugin.infra.monitor.PluginFileDetails;
import com.thoughtworks.go.plugin.infra.monitor.PluginJarChangeListener;
import com.thoughtworks.go.plugin.infra.plugininfo.DefaultPluginRegistry;
import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginDescriptor;
import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginDescriptorBuilder;
import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginOSGiManifest;
import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginOSGiManifestGenerator;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import static com.thoughtworks.go.util.SystemEnvironment.PLUGIN_ACTIVATOR_JAR_PATH;
@Component
public class DefaultPluginJarChangeListener implements PluginJarChangeListener {
private static final String ACTIVATOR_JAR_NAME = GoPluginOSGiManifest.ACTIVATOR_JAR_NAME;
private static Logger LOGGER = Logger.getLogger(DefaultPluginJarChangeListener.class);
private final DefaultPluginRegistry registry;
private final GoPluginOSGiManifestGenerator osgiManifestGenerator;
private final GoPluginOSGiFramework goPluginOSGiFramework;
private final SystemEnvironment systemEnvironment;
private GoPluginDescriptorBuilder goPluginDescriptorBuilder;
@Autowired
public DefaultPluginJarChangeListener(DefaultPluginRegistry registry, GoPluginOSGiManifestGenerator osgiManifestGenerator, GoPluginOSGiFramework goPluginOSGiFramework,
GoPluginDescriptorBuilder goPluginDescriptorBuilder, SystemEnvironment systemEnvironment) {
this.registry = registry;
this.osgiManifestGenerator = osgiManifestGenerator;
this.goPluginOSGiFramework = goPluginOSGiFramework;
this.goPluginDescriptorBuilder = goPluginDescriptorBuilder;
this.systemEnvironment = systemEnvironment;
}
@Override
public void pluginJarAdded(PluginFileDetails pluginFileDetails) {
GoPluginDescriptor descriptor = goPluginDescriptorBuilder.build(pluginFileDetails.file(), pluginFileDetails.isBundledPlugin());
GoPluginDescriptor existingDescriptor = registry.getPluginByIdOrFileName(descriptor.id(), descriptor.fileName());
validateIfExternalPluginRemovingBundledPlugin(descriptor, existingDescriptor);
validatePluginCompatibilityWithCurrentOS(descriptor);
addPlugin(pluginFileDetails, descriptor);
}
@Override
public void pluginJarUpdated(PluginFileDetails pluginFileDetails) {
GoPluginDescriptor descriptor = goPluginDescriptorBuilder.build(pluginFileDetails.file(), pluginFileDetails.isBundledPlugin());
GoPluginDescriptor existingDescriptor = registry.getPluginByIdOrFileName(descriptor.id(), descriptor.fileName());
validateIfExternalPluginRemovingBundledPlugin(descriptor, existingDescriptor);
validateIfSamePluginUpdated(descriptor, existingDescriptor);
validatePluginCompatibilityWithCurrentOS(descriptor);
removePlugin(descriptor);
addPlugin(pluginFileDetails, descriptor);
}
@Override
public void pluginJarRemoved(PluginFileDetails pluginFileDetails) {
GoPluginDescriptor existingDescriptor = registry.getPluginByIdOrFileName(null, pluginFileDetails.file().getName());
if(existingDescriptor==null){
return;
}
boolean externalPlugin = !pluginFileDetails.isBundledPlugin();
boolean bundledPlugin = existingDescriptor.isBundledPlugin();
boolean externalPluginWithSameIdAsBundledPlugin = bundledPlugin && externalPlugin;
if (externalPluginWithSameIdAsBundledPlugin) {
LOGGER.info(
String.format("External Plugin file '%s' having same name as bundled plugin file has been removed. "
+ "Refusing to unload bundled plugin with id: '%s'",
pluginFileDetails.file(), existingDescriptor.id()));
return;
}
removePlugin(existingDescriptor);
}
private void addPlugin(PluginFileDetails pluginFileDetails, GoPluginDescriptor descriptor) {
explodePluginJarToBundleDir(pluginFileDetails.file(), descriptor.bundleLocation());
installActivatorJarToBundleDir(descriptor.bundleLocation());
registry.loadPlugin(descriptor);
refreshBundle(descriptor);
}
private void removePlugin(GoPluginDescriptor descriptor) {
GoPluginDescriptor descriptorOfRemovedPlugin = registry.unloadPlugin(descriptor);
goPluginOSGiFramework.unloadPlugin(descriptorOfRemovedPlugin);
boolean bundleLocationHasBeenDeleted = FileUtils.deleteQuietly(descriptorOfRemovedPlugin.bundleLocation());
if (!bundleLocationHasBeenDeleted) {
throw new RuntimeException(String.format("Failed to remove bundle jar %s from bundle location %s", descriptorOfRemovedPlugin.fileName(), descriptorOfRemovedPlugin.bundleLocation()));
}
}
private void validateIfExternalPluginRemovingBundledPlugin(PluginFileDetails pluginFileDetails, GoPluginDescriptor existingDescriptor) {
if (existingDescriptor != null && existingDescriptor.isBundledPlugin() && !pluginFileDetails.isBundledPlugin()) {
throw new RuntimeException(String.format("Found bundled plugin with ID: [%s], external plugin could not be loaded", existingDescriptor.id()));
}
}
private void validateIfExternalPluginRemovingBundledPlugin(GoPluginDescriptor newDescriptor, GoPluginDescriptor existingDescriptor) {
if (existingDescriptor != null && existingDescriptor.isBundledPlugin() && !newDescriptor.isBundledPlugin()) {
throw new RuntimeException(String.format("Found bundled plugin with ID: [%s], external plugin could not be loaded", existingDescriptor.id()));
}
}
private void validateIfSamePluginUpdated(GoPluginDescriptor descriptor, GoPluginDescriptor existingDescriptor) {
if (existingDescriptor != null && !existingDescriptor.fileName().equals(descriptor.fileName())) {
throw new RuntimeException("Found another plugin with ID: " + existingDescriptor.id());
}
}
private void validatePluginCompatibilityWithCurrentOS(GoPluginDescriptor descriptor) {
String currentOS = systemEnvironment.getOperatingSystemFamilyName();
if (!descriptor.isCurrentOSValidForThisPlugin(currentOS)) {
List<String> messages = Arrays.asList(String.format("Plugin with ID (%s) is not valid: Incompatible with current operating system '%s'. Valid operating systems are: %s.",
descriptor.id(), currentOS, descriptor.about().targetOperatingSystems()));
descriptor.markAsInvalid(messages, null);
}
}
private void refreshBundle(GoPluginDescriptor descriptor) {
if (!descriptor.isInvalid()) {
osgiManifestGenerator.updateManifestOf(descriptor);
goPluginOSGiFramework.loadPlugin(descriptor);
providePluginDescriptorToThePlugin(descriptor);
}
}
private void providePluginDescriptorToThePlugin(final GoPluginDescriptor descriptor) {
if (descriptor.isInvalid()) {
LOGGER.debug("Descriptor Invalid skipping plugin descriptor callback.");
return;
}
if (!goPluginOSGiFramework.hasReferenceFor(PluginDescriptorAware.class, descriptor.id())) {
return;
}
goPluginOSGiFramework.doOnAllWithExceptionHandlingForPlugin(
PluginDescriptorAware.class, descriptor.id(), new Action<PluginDescriptorAware>() {
@Override
public void execute(PluginDescriptorAware descriptorAwarePlugin, GoPluginDescriptor pluginDescriptor) {
descriptorAwarePlugin.setPluginDescriptor(pluginDescriptor);
}
}, new ExceptionHandler<PluginDescriptorAware>() {
@Override
public void handleException(PluginDescriptorAware obj, Throwable t) {
LOGGER.warn("Set Plugin Descriptor Call failed for plugin: " + descriptor.id(),t);
}
}
);
}
void explodePluginJarToBundleDir(File file, File location) {
try {
wipePluginBundleDirectory(location);
ZipUtil zipUtil = new ZipUtil();
zipUtil.unzip(file, location);
} catch (IOException e) {
throw new RuntimeException(String.format("Failed to copy plugin jar %s to bundle location %s", file, location), e);
}
}
void installActivatorJarToBundleDir(File pluginBundleExplodedDir) {
URL activatorJar = findAndValidateActivatorJar();
File pluginActivatorJarDestination = new File(new File(pluginBundleExplodedDir, GoPluginOSGiManifest.PLUGIN_DEPENDENCY_DIR), ACTIVATOR_JAR_NAME);
try {
FileUtils.copyURLToFile(activatorJar, pluginActivatorJarDestination);
} catch (IOException e) {
throw new RuntimeException(String.format("Failed to copy activator jar %s to bundle dependency dir: %s", activatorJar, pluginActivatorJarDestination), e);
}
}
private void wipePluginBundleDirectory(File pluginBundleDirectory) {
if (pluginBundleDirectory.exists() && !FileUtils.deleteQuietly(pluginBundleDirectory)) {
throw new RuntimeException(String.format("Failed to delete bundle directory %s", pluginBundleDirectory));
}
pluginBundleDirectory.mkdirs();
}
private URL findAndValidateActivatorJar() {
URL activatorJar = getClass().getClassLoader().getResource(systemEnvironment.get(PLUGIN_ACTIVATOR_JAR_PATH));
if (activatorJar == null) {
throw new RuntimeException("Unable to load plugins. Cannot find activator jar in classpath.");
}
return activatorJar;
}
}