/* * Copyright © 2014-2015 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.deploy; import co.cask.cdap.api.Config; import co.cask.cdap.api.app.Application; import co.cask.cdap.api.app.ApplicationSpecification; import co.cask.cdap.app.DefaultAppConfigurer; import co.cask.cdap.app.DefaultApplicationContext; import co.cask.cdap.app.deploy.ConfigResponse; import co.cask.cdap.app.deploy.Configurator; import co.cask.cdap.app.program.ManifestFields; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.common.conf.Constants; import co.cask.cdap.common.io.CaseInsensitiveEnumTypeAdapterFactory; import co.cask.cdap.common.lang.jar.BundleJarUtil; import co.cask.cdap.common.utils.DirUtils; import co.cask.cdap.internal.app.ApplicationSpecificationAdapter; import co.cask.cdap.internal.app.runtime.artifact.ArtifactRepository; import co.cask.cdap.internal.app.runtime.artifact.Artifacts; import co.cask.cdap.internal.app.runtime.artifact.CloseableClassLoader; import co.cask.cdap.internal.app.runtime.plugin.PluginInstantiator; import co.cask.cdap.internal.io.ReflectionSchemaGenerator; import co.cask.cdap.proto.Id; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.common.io.CharStreams; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; import org.apache.twill.filesystem.Location; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.lang.reflect.Type; import java.util.jar.Manifest; import javax.annotation.Nullable; /** * In Memory Configurator doesn't spawn a external process, but * does this in memory. * * @see SandboxConfigurator */ public final class InMemoryConfigurator implements Configurator { private static final Logger LOG = LoggerFactory.getLogger(InMemoryConfigurator.class); /** * JAR file path. */ private final Location artifact; private final CConfiguration cConf; private final String configString; private final File baseUnpackDir; // this is the namespace that the app will be in, which may be different than the namespace of the artifact. // if the artifact is a system artifact, the namespace will be the system namespace. private final Id.Namespace appNamespace; private ArtifactRepository artifactRepository; // these field provided if going through artifact code path, but not through template code path // this is temporary until we can remove templates. (CDAP-2662). private String appClassName; private Id.Artifact artifactId; public InMemoryConfigurator(CConfiguration cConf, Id.Namespace appNamespace, Id.Artifact artifactId, String appClassName, Location artifact, @Nullable String configString, ArtifactRepository artifactRepository) { Preconditions.checkNotNull(artifact); this.cConf = cConf; this.appNamespace = appNamespace; this.artifactId = artifactId; this.appClassName = appClassName; this.artifact = artifact; this.configString = configString; this.artifactRepository = artifactRepository; this.baseUnpackDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR), cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile(); } /** * Executes the <code>Application.configure</code> within the same JVM. * <p> * This method could be dangerous and should be used only in standalone mode. * </p> * * @return A instance of {@link ListenableFuture}. */ @Override public ListenableFuture<ConfigResponse> config() { SettableFuture<ConfigResponse> result = SettableFuture.create(); try { if (appClassName == null) { readAppClassName(); } try (CloseableClassLoader artifactClassLoader = artifactRepository.createArtifactClassLoader(artifact)) { Object appMain = artifactClassLoader.loadClass(appClassName).newInstance(); if (!(appMain instanceof Application)) { throw new IllegalStateException(String.format("Application main class is of invalid type: %s", appMain.getClass().getName())); } Application app = (Application) appMain; ConfigResponse response = createResponse(app); result.set(response); } return result; } catch (Throwable t) { LOG.error(t.getMessage(), t); return Futures.immediateFailedFuture(t); } } // remove once app templates are gone private void readAppClassName() throws IOException { // Load the JAR using the JAR class load and load the manifest file. Manifest manifest = BundleJarUtil.getManifest(artifact); Preconditions.checkArgument(manifest != null, "Failed to load manifest from %s", artifact); Preconditions.checkArgument(manifest.getMainAttributes() != null, "Failed to load manifest attributes from %s", artifact); appClassName = manifest.getMainAttributes().getValue(ManifestFields.MAIN_CLASS); Preconditions.checkArgument(appClassName != null && !appClassName.isEmpty(), "Main class attribute cannot be empty"); } private ConfigResponse createResponse(Application app) throws InstantiationException, IllegalAccessException, IOException { String specJson = getSpecJson(app, configString); return new DefaultConfigResponse(0, CharStreams.newReaderSupplier(specJson)); } private <T extends Config> String getSpecJson(Application<T> app, final String configString) throws IllegalAccessException, InstantiationException, IOException { File tempDir = DirUtils.createTempDir(baseUnpackDir); // This Gson cannot be static since it is used to deserialize user class. // Gson will keep a static map to class, hence will leak the classloader Gson gson = new GsonBuilder().registerTypeAdapterFactory(new CaseInsensitiveEnumTypeAdapterFactory()).create(); // Now, we call configure, which returns application specification. DefaultAppConfigurer configurer; try ( PluginInstantiator pluginInstantiator = new PluginInstantiator(cConf, app.getClass().getClassLoader(), tempDir) ) { configurer = new DefaultAppConfigurer(appNamespace, artifactId, app, configString, artifactRepository, pluginInstantiator); T appConfig; Type configType = Artifacts.getConfigType(app.getClass()); if (Strings.isNullOrEmpty(configString)) { //noinspection unchecked appConfig = ((Class<T>) configType).newInstance(); } else { try { appConfig = gson.fromJson(configString, configType); } catch (JsonSyntaxException e) { throw new IllegalArgumentException("Invalid JSON configuration was provided. Please check the syntax.", e); } } app.configure(configurer, new DefaultApplicationContext<>(appConfig)); } finally { try { DirUtils.deleteDirectoryContents(tempDir); } catch (IOException e) { LOG.warn("Exception raised when deleting directory {}", tempDir, e); } } ApplicationSpecification specification = configurer.createSpecification(); // Convert the specification to JSON. // We write the Application specification to output file in JSON format. // TODO: The SchemaGenerator should be injected return ApplicationSpecificationAdapter.create(new ReflectionSchemaGenerator()).toJson(specification); } }