/* * Copyright © 2016 Cask Data, 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 co.cask.cdap.internal.app.runtime.plugin; import co.cask.cdap.api.artifact.ArtifactId; import co.cask.cdap.api.plugin.EndpointPluginContext; import co.cask.cdap.api.plugin.PluginClass; import co.cask.cdap.common.ArtifactNotFoundException; import co.cask.cdap.common.NotFoundException; import co.cask.cdap.common.ServiceUnavailableException; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.utils.DirUtils; import co.cask.cdap.internal.app.runtime.DefaultEndpointPluginContext; import co.cask.cdap.internal.app.runtime.artifact.ArtifactDescriptor; import co.cask.cdap.internal.app.runtime.artifact.ArtifactDetail; import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository; import co.cask.cdap.internal.app.runtime.artifact.CloseableClassLoader; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.artifact.ArtifactRange; import co.cask.cdap.proto.id.NamespaceId; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.cache.Weigher; import com.google.common.io.Closeables; import com.google.common.util.concurrent.AbstractIdleService; import com.google.inject.Inject; import com.google.inject.Singleton; import org.apache.twill.filesystem.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import javax.ws.rs.Path; /** * To find, instantiate and invoke methods in plugin artifacts */ @Singleton public class PluginService extends AbstractIdleService { private static final Logger LOG = LoggerFactory.getLogger(PluginService.class); private final ArtifactRepository artifactRepository; private final File tmpDir; private final CConfiguration cConf; private final LoadingCache<ArtifactDescriptor, Instantiators> instantiators; private File stageDir; @Inject public PluginService(ArtifactRepository artifactRepository, CConfiguration cConf) { this.artifactRepository = artifactRepository; this.tmpDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR), cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile(); this.cConf = cConf; this.instantiators = CacheBuilder.newBuilder() .removalListener(new InstantiatorsRemovalListener()) .maximumWeight(100) .weigher(new Weigher<ArtifactDescriptor, Instantiators>() { @Override public int weigh(ArtifactDescriptor key, Instantiators value) { return value.size(); } }) .expireAfterAccess(1, TimeUnit.HOURS) .build(new InstantiatorsCacheLoader()); } /** * Given plugin artifact, find the parent artifact of plugin, after creating classloader with parent artifact * and plugin, we invoke the plugin method in the plugin identified by type and name and return the response. * @param namespace namespace * @param artifactId plugin artifact id * @param pluginType type of the plugin * @param pluginName name of the plugin * @param methodName name of the method * @return {@link PluginEndpoint} * @throws IOException * @throws NotFoundException * @throws ClassNotFoundException */ public PluginEndpoint getPluginEndpoint(NamespaceId namespace, Id.Artifact artifactId, String pluginType, String pluginName, String methodName) throws IOException, NotFoundException, ClassNotFoundException { // should not happen if (!isRunning()) { throw new ServiceUnavailableException("Plugin Service is not running currently"); } ArtifactDetail artifactDetail = artifactRepository.getArtifact(artifactId); return getPluginEndpoint(namespace, artifactDetail, pluginType, pluginName, pickParentArtifact(artifactDetail, artifactId), methodName); } private ArtifactDescriptor pickParentArtifact(ArtifactDetail artifactDetail, Id.Artifact artifact) throws ArtifactNotFoundException, IOException { // get parent artifacts Set<ArtifactRange> parentArtifactRanges = artifactDetail.getMeta().getUsableBy(); if (parentArtifactRanges.isEmpty()) { throw new ArtifactNotFoundException(artifact); } // just pick the first parent artifact from the set. ArtifactRange parentArtifactRange = parentArtifactRanges.iterator().next(); List<ArtifactDetail> artifactDetails = artifactRepository.getArtifacts(parentArtifactRange); if (artifactDetails.isEmpty()) { // should not happen throw new ArtifactNotFoundException(artifact); } // return the first one from the artifact details list. return artifactDetails.get(0).getDescriptor(); } @Override protected void startUp() { stageDir = DirUtils.createTempDir(tmpDir); } @Override protected void shutDown() { instantiators.invalidateAll(); try { DirUtils.deleteDirectoryContents(stageDir); } catch (IOException e) { LOG.error("Error while deleting directory in PluginService", e); } } /** * A RemovalListener for closing classloader and plugin instantiators. */ private static final class InstantiatorsRemovalListener implements RemovalListener<ArtifactDescriptor, Instantiators> { @Override public void onRemoval(RemovalNotification<ArtifactDescriptor, Instantiators> notification) { Closeables.closeQuietly(notification.getValue()); } } private class Instantiators implements Closeable { private final CloseableClassLoader parentClassLoader; private final Map<ArtifactDescriptor, InstantiatorInfo> instantiatorInfoMap; private final File pluginDir; private Instantiators(ArtifactDescriptor parentArtifactDescriptor) throws IOException { this.parentClassLoader = artifactRepository.createArtifactClassLoader(parentArtifactDescriptor.getLocation()); this.instantiatorInfoMap = new ConcurrentHashMap<>(); this.pluginDir = DirUtils.createTempDir(stageDir); } private boolean hasArtifactChanged(ArtifactDescriptor artifactDescriptor) { if (instantiatorInfoMap.containsKey(artifactDescriptor) && !instantiatorInfoMap.get(artifactDescriptor).getArtifactLocation().equals(artifactDescriptor.getLocation())) { return true; } return false; } private void addInstantiatorAndAddArtifact(ArtifactDetail artifactDetail, ArtifactId artifactId) throws IOException { PluginInstantiator instantiator = new PluginInstantiator(cConf, parentClassLoader, pluginDir); instantiatorInfoMap.put(artifactDetail.getDescriptor(), new InstantiatorInfo(artifactDetail.getDescriptor().getLocation(), instantiator)); instantiator.addArtifact(artifactDetail.getDescriptor().getLocation(), artifactId); } private PluginInstantiator getPluginInstantiator(ArtifactDetail artifactDetail, ArtifactId artifactId) throws IOException { if (!instantiatorInfoMap.containsKey(artifactDetail.getDescriptor())) { addInstantiatorAndAddArtifact(artifactDetail, artifactId); } else if (hasArtifactChanged(artifactDetail.getDescriptor())) { instantiatorInfoMap.remove(artifactDetail.getDescriptor()); addInstantiatorAndAddArtifact(artifactDetail, artifactId); } return instantiatorInfoMap.get(artifactDetail.getDescriptor()).getPluginInstantiator(); } private int size() { return instantiatorInfoMap.size(); } @Override public void close() throws IOException { for (InstantiatorInfo instantiatorInfo : instantiatorInfoMap.values()) { Closeables.closeQuietly(instantiatorInfo.getPluginInstantiator()); } Closeables.closeQuietly(parentClassLoader); DirUtils.deleteDirectoryContents(pluginDir); } } private class InstantiatorInfo { private final Location artifactLocation; private final PluginInstantiator pluginInstantiator; private InstantiatorInfo(Location artifactLocation, PluginInstantiator pluginInstantiator) { this.artifactLocation = artifactLocation; this.pluginInstantiator = pluginInstantiator; } private Location getArtifactLocation() { return artifactLocation; } private PluginInstantiator getPluginInstantiator() { return pluginInstantiator; } } /** * A CacheLoader for creating Instantiators. */ private final class InstantiatorsCacheLoader extends CacheLoader<ArtifactDescriptor, Instantiators> { @Override public Instantiators load(ArtifactDescriptor parentArtifactDescriptor) throws Exception { return new Instantiators(parentArtifactDescriptor); } } private PluginEndpoint getPluginEndpoint(NamespaceId namespace, ArtifactDetail artifactDetail, String pluginType, String pluginName, ArtifactDescriptor parentArtifactDescriptor, String methodName) throws NotFoundException, IOException, ClassNotFoundException { Id.Artifact artifactId = Id.Artifact.from(namespace.toId(), artifactDetail.getDescriptor().getArtifactId()); Set<PluginClass> pluginClasses = artifactDetail.getMeta().getClasses().getPlugins(); PluginClass pluginClass = null; for (PluginClass plugin : pluginClasses) { if (plugin.getName().equals(pluginName) && plugin.getType().equals(pluginType)) { // plugin type and name matched, next check for endpoint method presence if (plugin.getEndpoints() == null || !plugin.getEndpoints().contains(methodName)) { throw new NotFoundException( String.format("Plugin with type: %s name: %s found, " + "but Endpoint %s was not found", pluginType, pluginName, methodName)); } pluginClass = plugin; } } if (pluginClass == null) { throw new NotFoundException( String.format("No Plugin with type : %s, name: %s was found", pluginType, pluginName)); } // initialize parent classloader and plugin instantiator Instantiators instantiators = this.instantiators.getUnchecked(parentArtifactDescriptor); PluginInstantiator pluginInstantiator = instantiators.getPluginInstantiator(artifactDetail, artifactId.toArtifactId()); // we pass the parent artifact to endpoint plugin context, // as plugin method will use this context to load other plugins. DefaultEndpointPluginContext defaultEndpointPluginContext = new DefaultEndpointPluginContext(namespace, artifactRepository, pluginInstantiator, Id.Artifact.from(namespace.toId(), parentArtifactDescriptor.getArtifactId())); return getPluginEndpoint(pluginInstantiator, artifactId, pluginClass, methodName, defaultEndpointPluginContext); } /** * load and instantiate the plugin and return {@link PluginEndpoint} * which can be used to invoke plugin method with request object. * * @param pluginInstantiator to instantiate plugin instances. * @param artifact artifact of the plugin * @param pluginClass class having the plugin method to invoke * @param endpointName name of the endpoint to invoke * @param endpointPluginContext endpoint plugin context that can optionally be passed to the method * @throws IOException if there was an exception getting the classloader * @throws ClassNotFoundException if plugin class cannot be loaded * @throws NotFoundException Not Found exception thrown if no matching plugin found. */ private PluginEndpoint getPluginEndpoint(final PluginInstantiator pluginInstantiator, Id.Artifact artifact, PluginClass pluginClass, String endpointName, final DefaultEndpointPluginContext endpointPluginContext) throws IOException, ClassNotFoundException, NotFoundException { ClassLoader pluginClassLoader = pluginInstantiator.getArtifactClassLoader(artifact.toArtifactId()); Class pluginClassLoaded = pluginClassLoader.loadClass(pluginClass.getClassName()); final Object pluginInstance = pluginInstantiator.newInstanceWithoutConfig(artifact.toArtifactId(), pluginClass); Method[] methods = pluginClassLoaded.getMethods(); for (final Method method : methods) { Path pathAnnotation = method.getAnnotation(Path.class); // method should have path annotation else continue if (pathAnnotation == null || !pathAnnotation.value().equals(endpointName)) { continue; } return new PluginEndpoint() { @Override public java.lang.reflect.Type getMethodParameterType() throws IllegalArgumentException { if (method.getParameterTypes().length == 0) { // should not happen, checks should have happened during deploy artifact. throw new IllegalArgumentException("No Method parameter type found"); } return method.getGenericParameterTypes()[0]; } @Override public java.lang.reflect.Type getResultType() { return method.getGenericReturnType(); } @Override public Object invoke(Object request) throws IOException, ClassNotFoundException, InvocationTargetException, IllegalAccessException, IllegalArgumentException { if (method.getParameterTypes().length == 2 && EndpointPluginContext.class.isAssignableFrom(method.getParameterTypes()[1])) { return method.invoke(pluginInstance, request, endpointPluginContext); } else if (method.getParameterTypes().length == 1) { return method.invoke(pluginInstance, request); } else { throw new IllegalArgumentException( String.format("Only method with 1 parameter and optional EndpointPluginContext as 2nd parameter is " + "allowed in Plugin endpoint method, Found %s parameters", method.getParameterTypes().length)); } } }; }; // cannot find the endpoint in plugin method. should not happen as this is checked earlier. throw new NotFoundException("Could not find the plugin method with the requested method endpoint {}", endpointName); } }