summaryrefslogtreecommitdiffstats
path: root/application/src/main/java/com/yahoo/application/Application.java
diff options
context:
space:
mode:
Diffstat (limited to 'application/src/main/java/com/yahoo/application/Application.java')
-rw-r--r--application/src/main/java/com/yahoo/application/Application.java661
1 files changed, 661 insertions, 0 deletions
diff --git a/application/src/main/java/com/yahoo/application/Application.java b/application/src/main/java/com/yahoo/application/Application.java
new file mode 100644
index 00000000000..9654a798353
--- /dev/null
+++ b/application/src/main/java/com/yahoo/application/Application.java
@@ -0,0 +1,661 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.application;
+
+import com.google.common.annotations.Beta;
+import com.yahoo.application.container.JDisc;
+import com.yahoo.application.container.impl.StandaloneContainerRunner;
+import com.yahoo.application.content.ContentCluster;
+import com.yahoo.config.*;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.model.NullConfigModelRegistry;
+import com.yahoo.config.model.application.provider.FilesApplicationPackage;
+import com.yahoo.config.model.deploy.DeployState;
+import com.yahoo.docproc.DocumentProcessor;
+import com.yahoo.io.IOUtils;
+import com.yahoo.jdisc.handler.RequestHandler;
+import com.yahoo.jdisc.service.ClientProvider;
+import com.yahoo.jdisc.service.ServerProvider;
+import com.yahoo.search.Searcher;
+import com.yahoo.search.rendering.Renderer;
+import com.yahoo.text.StringUtilities;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.model.VespaModel;
+import org.jboss.netty.channel.ChannelException;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Field;
+import java.net.BindException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+
+/**
+ * Contains one or more containers built from services.xml.
+ * Other services present in the services.xml file might be mocked in future versions.
+ * <p>
+ * Currently, only a single top level JDisc Container is allowed. Other clusters are ignored.
+ *
+ * @author tonytv
+ */
+@Beta
+public final class Application implements AutoCloseable {
+
+ private final JDisc container;
+ private final List<ContentCluster> contentClusters;
+ private final Path path;
+ private final boolean deletePathWhenClosing;
+
+ // For internal use only
+ Application(Path path, Networking networking, boolean deletePathWhenClosing) {
+ this.path = path;
+ this.deletePathWhenClosing = deletePathWhenClosing;
+ contentClusters = ContentCluster.fromPath(path);
+ container = JDisc.fromPath(path, networking, createVespaModel().configModelRepo());
+ }
+
+ @Beta
+ public static Application fromBuilder(Builder builder) throws Exception {
+ return builder.build();
+ }
+
+ /**
+ * Factory method to create an Application from an XML String. Note that any components that are referenced in
+ * the XML must be present on the classpath. To deploy OSGi bundles in memory,
+ * use {@link Application#fromApplicationPackage(Path, Networking)}.
+ *
+ * @param xml the XML configuration to use
+ * @return a new JDisc instance
+ */
+ public static Application fromServicesXml(String xml, Networking networking) {
+ Path applicationDir = StandaloneContainerRunner.createApplicationPackage(xml);
+ return new Application(applicationDir, networking, true);
+ }
+
+ /**
+ * Factory method to create an Application from an application package.
+ * This method allows deploying OSGi bundles(contained in the components subdirectory).
+ * All the OSGi bundles will share the same class loader.
+ *
+ * @param path the reference to the application package to use
+ * @return a new JDisc instance
+ */
+ public static Application fromApplicationPackage(Path path, Networking networking) {
+ return new Application(path, networking, false);
+ }
+
+ /**
+ * Factory method to create an Application from an application package.
+ * This method allows deploying OSGi bundles(contained in the components subdirectory).
+ * All the OSGi bundles will share the same class loader.
+ *
+ * @param file the reference to the application package to use
+ * @return a new JDisc instance
+ */
+ public static Application fromApplicationPackage(File file, Networking networking) {
+ return fromApplicationPackage(file.toPath(), networking);
+ }
+
+ private VespaModel createVespaModel() {
+ try {
+ DeployState deployState = new DeployState.Builder()
+ .applicationPackage(FilesApplicationPackage.fromFile(path.toFile()))
+ .deployLogger((level, s) -> { })
+ .build();
+ return new VespaModel(new NullConfigModelRegistry(), deployState);
+ } catch (IOException | SAXException e) {
+ throw new IllegalArgumentException("Error creating application from '" + path + "'", e);
+ }
+ }
+
+ /**
+ * @param id from the jdisc element in services xml. Default id in services.xml is "jdisc"
+ */
+ public JDisc getJDisc(String id) {
+ return container;
+ }
+
+ /**
+ * Shuts down all services.
+ */
+ @Override
+ public void close() {
+ container.close();
+ if (deletePathWhenClosing)
+ IOUtils.recursiveDeleteDir(path.toFile());
+ }
+
+ /**
+ * A wrapper around ApplicationBuilder that generates a services.xml
+ */
+ @Beta
+ public static class Builder {
+ private static final ThreadLocal<Random> random = new ThreadLocal<>();
+ private static final String DEFAULT_CHAIN = "default";
+
+ private final Map<String, Container> containers = new LinkedHashMap<>();
+ private final Path path;
+ private Networking networking = Networking.disable;
+
+ public Builder() throws IOException {
+ this.path = makeTempDir("app", "standalone").toPath();
+ }
+
+ public Builder container(String id, Container container) {
+ if (containers.size() > 0) {
+ throw new RuntimeException("Only a single JDisc container is currently supported.");
+ }
+ containers.put(id, container);
+ return this;
+ }
+
+ /**
+ * Create a temporary directory using @{link File.createTempFile()}, but creating
+ * a directory instead of a file.
+ *
+ * @param prefix directory prefix
+ * @param suffix directory suffix
+ * @return The created directory
+ * @throws IOException if the temporary directory could not be created
+ */
+ private static File makeTempDir(String prefix, String suffix) throws IOException {
+ File tmpDir = File.createTempFile(prefix, suffix, getTempDir());
+ if (!tmpDir.delete()) {
+ throw new RuntimeException("Could not delete temp directory: " + tmpDir);
+ }
+ if (!tmpDir.mkdirs()) {
+ throw new RuntimeException("Could not create temp directory: " + tmpDir);
+ }
+ return tmpDir;
+ }
+
+ /**
+ * Get the temporary directory
+ *
+ * @return The temporary directory File object
+ */
+ private static File getTempDir() {
+ String rootPath = getResourceFile("/");
+ String tmpPath = rootPath + "/tmp/";
+ File tmpDir = new File(tmpPath);
+ if (!tmpDir.exists() && !tmpDir.mkdirs()) {
+ if (!tmpDir.exists()) { // possible race condition may cause mkdirs() to fail, check a second time before failing
+ throw new RuntimeException("Could not create temp dir: " + tmpDir.getAbsolutePath());
+ }
+ }
+ if (!tmpDir.isDirectory()) {
+ throw new RuntimeException("Temp dir path is not a directory: " + tmpDir.getAbsolutePath());
+ }
+ return tmpDir;
+ }
+
+ /**
+ * Get the file name (path) of a resource or fail if it can not be found
+ *
+ * @param resource Name of desired resource
+ * @return Path of resource
+ */
+ private static String getResourceFile(String resource) {
+ URL resourceUrl = Application.class.getResource(resource);
+ if (resourceUrl == null || resourceUrl.getFile() == null || resourceUrl.getFile().isEmpty()) {
+ throw new RuntimeException("Could not access resource: " + resource);
+ }
+
+ return resourceUrl.getFile();
+ }
+
+ // copy from com.yahoo.application.ApplicationBuilder
+ private void createFile(final Path path, final String content) throws IOException {
+ Files.createDirectories(path.getParent());
+ Files.write(path, Utf8.toBytes(content));
+ }
+
+ /**
+ * @return a random number between 2000 and 62000
+ */
+ private static int getRandomPort() {
+ Random r = random.get();
+ if (r == null) {
+ r = new Random(System.currentTimeMillis());
+ random.set(r);
+ }
+ return r.nextInt(60000) + 2000;
+ }
+
+ /**
+ * @param name name of document type (search definition)
+ * @param searchDefinition add this search definition to the application
+ * @return builder
+ * @throws java.io.IOException e.g.if file not found
+ */
+ public Builder documentType(final String name, final String searchDefinition) throws IOException {
+ Path path = nestedResource(ApplicationPackage.SEARCH_DEFINITIONS_DIR, name, ApplicationPackage.SD_NAME_SUFFIX);
+ createFile(path, searchDefinition);
+ return this;
+ }
+
+ public Builder expressionInclude(final String name, final String searchDefinition) throws IOException {
+ Path path = nestedResource(ApplicationPackage.SEARCH_DEFINITIONS_DIR, name, ApplicationPackage.RANKEXPRESSION_NAME_SUFFIX);
+ createFile(path, searchDefinition);
+ return this;
+ }
+
+ /**
+ * @param name name of rank expression
+ * @param rankExpressionContent add this rank expression to the application
+ * @return builder
+ * @throws java.io.IOException e.g.if file not found
+ */
+ public Builder rankExpression(final String name, final String rankExpressionContent) throws IOException {
+ Path path = nestedResource(ApplicationPackage.SEARCH_DEFINITIONS_DIR, name, ApplicationPackage.RANKEXPRESSION_NAME_SUFFIX);
+ createFile(path, rankExpressionContent);
+ return this;
+ }
+
+ /**
+ * @param name name of query profile
+ * @param queryProfile add this queyr profile to the application
+ * @return builder
+ * @throws java.io.IOException e.g.if file not found
+ */
+ public Builder queryProfile(final String name, final String queryProfile) throws IOException {
+ Path path = nestedResource(ApplicationPackage.QUERY_PROFILES_DIR, name, ".xml");
+ createFile(path, queryProfile);
+ return this;
+ }
+
+ /**
+ * @param name name of query profile type
+ * @param queryProfileType add this query profile type to the application
+ * @return builder
+ * @throws java.io.IOException e.g.if file not found
+ */
+ public Builder queryProfileType(final String name, final String queryProfileType) throws IOException {
+ Path path = nestedResource(ApplicationPackage.QUERY_PROFILE_TYPES_DIR, name, ".xml");
+ createFile(path, queryProfileType);
+ return this;
+ }
+
+ // copy from com.yahoo.application.ApplicationBuilder
+ private Path nestedResource(final com.yahoo.path.Path nestedPath, final String name, final String fileType) {
+ String nameWithoutSuffix = StringUtilities.stripSuffix(name, fileType);
+ return path.resolve(nestedPath.getRelative()).resolve(nameWithoutSuffix + fileType);
+ }
+
+ /**
+ * @param networking enable or disable networking (disabled by default)
+ * @return builder
+ */
+ public Builder networking(final Networking networking) {
+ this.networking = networking;
+ return this;
+ }
+
+ // generate the services xml and load the container
+ private Application build() throws Exception {
+ Application app = null;
+ Exception exception = null;
+
+ // if we get a bind exception, then retry a few times (may conflict with parallel test runs)
+ for (int i = 0; i < 5; i++) {
+ try {
+ generateXml();
+ app = new Application(path, networking, true);
+ break;
+ } catch (Error e) { // the container thinks this is really serious, in this case is it not in the cause is a BindException
+ // catch bind error and reset container
+ if (e.getCause() != null && e.getCause() instanceof ChannelException && e.getCause().getCause() != null && e.getCause().getCause() instanceof BindException) {
+ exception = (Exception) e.getCause().getCause();
+ com.yahoo.container.Container.resetInstance(); // this is needed to be able to recreate the container from config again
+ } else {
+ throw new Exception(e.getCause());
+ }
+ }
+ }
+
+ if (app == null) {
+ throw exception;
+ }
+ return app;
+ }
+
+ private void generateXml() throws Exception {
+ try (PrintWriter xml = new PrintWriter(Files.newOutputStream(path.resolve("services.xml")))) {
+ xml.println("<?xml version=\"1.0\" encoding=\"utf-8\" ?>");
+ //xml.println("<services version=\"1.0\">");
+ for (Map.Entry<String, Container> entry : containers.entrySet()) {
+ entry.getValue().build(xml, entry.getKey(), (networking == Networking.enable ? getRandomPort() : -1));
+ }
+ //xml.println("</services>");
+ }
+ }
+
+ public static class Container {
+ private final Map<String, List<ComponentItem<? extends DocumentProcessor>>> docprocs = new LinkedHashMap<>();
+ private final Map<String, List<ComponentItem<? extends Searcher>>> searchers = new LinkedHashMap<>();
+ private final List<ComponentItem<? extends Renderer>> renderers = new ArrayList<>();
+ private final List<ComponentItem<? extends RequestHandler>> handlers = new ArrayList<>();
+ private final List<ComponentItem<? extends ClientProvider>> clients = new ArrayList<>();
+ private final List<ComponentItem<? extends ServerProvider>> servers = new ArrayList<>();
+ private final List<ComponentItem<?>> components = new ArrayList<>();
+ private final List<ConfigInstance> configs = new ArrayList<>();
+ private boolean enableSearch = false;
+
+ private static class ComponentItem<T> {
+ private String id;
+ private Class<? extends T> component;
+ private List<ConfigInstance> configs = new ArrayList<>();
+
+ public ComponentItem(String id, Class<? extends T> component, ConfigInstance... configs) {
+ this.id = id;
+ this.component = component;
+ if (configs != null) {
+ Collections.addAll(this.configs, configs);
+ }
+ }
+ }
+
+ /**
+ * @param docproc add this docproc to the default document processing chain
+ * @return builder
+ */
+ public Container documentProcessor(final Class<? extends DocumentProcessor> docproc) {
+ return documentProcessor(DEFAULT_CHAIN, docproc);
+ }
+
+ /**
+ * @param chainName chain name to add docproc
+ * @param docproc add this docproc to the document processing chain
+ * @param configs local docproc configs
+ * @return builder
+ */
+ public Container documentProcessor(final String chainName, final Class<? extends DocumentProcessor> docproc, ConfigInstance... configs) {
+ return documentProcessor(docproc.getName(), chainName, docproc, configs);
+ }
+
+ /**
+ * @param id component id
+ * @param chainName chain name to add docproc
+ * @param docproc add this docproc to the document processing chain
+ * @param configs local docproc configs
+ * @return builder
+ */
+ public Container documentProcessor(String id, final String chainName, final Class<? extends DocumentProcessor> docproc, ConfigInstance... configs) {
+ List<ComponentItem<? extends DocumentProcessor>> chain = docprocs.get(chainName);
+ if (chain == null) {
+ chain = new ArrayList<>();
+ docprocs.put(chainName, chain);
+ }
+ chain.add(new ComponentItem<>(id, docproc, configs));
+ return this;
+ }
+
+ /**
+ * @param enableSearch if true, enable search even without any searchers defined
+ * @return builder
+ */
+ public Container search(boolean enableSearch) {
+ this.enableSearch = enableSearch;
+ return this;
+ }
+
+ /**
+ * @param searcher add this searcher to the default search chain
+ * @return builder
+ */
+ public Container searcher(final Class<? extends Searcher> searcher) {
+ return searcher(DEFAULT_CHAIN, searcher);
+ }
+
+ /**
+ * @param chainName chain name to add searcher
+ * @param searcher add this searcher to the search chain
+ * @param configs local searcher configs
+ * @return builder
+ */
+ public Container searcher(final String chainName, final Class<? extends Searcher> searcher, ConfigInstance... configs) {
+ return searcher(searcher.getName(), chainName, searcher, configs);
+ }
+
+ /**
+ * @param id component id
+ * @param chainName chain name to add searcher
+ * @param searcher add this searcher to the search chain
+ * @param configs local searcher configs
+ * @return builder
+ */
+ public Container searcher(String id, final String chainName, final Class<? extends Searcher> searcher, ConfigInstance... configs) {
+ List<ComponentItem<? extends Searcher>> chain = searchers.get(chainName);
+ if (chain == null) {
+ chain = new ArrayList<>();
+ searchers.put(chainName, chain);
+ }
+ chain.add(new ComponentItem<>(id, searcher, configs));
+ return this;
+ }
+
+ /**
+ * @param id component id, enable template with ?format=id or ?presentation.format=id
+ * @param renderer add this renderer
+ * @param configs local renderer configs
+ * @return builder
+ */
+ public Container renderer(String id, final Class<? extends Renderer> renderer, ConfigInstance... configs) {
+ renderers.add(new ComponentItem<>(id, renderer, configs));
+ return this;
+ }
+
+ /**
+ * @param binding binding string
+ * @param handler the handler class
+ * @return builder
+ */
+ public Container handler(final String binding, final Class<? extends RequestHandler> handler) {
+ handlers.add(new ComponentItem<>(binding, handler));
+ return this;
+ }
+
+ /**
+ * @param binding binding string
+ * @param client the client class
+ * @return builder
+ */
+ public Container client(final String binding, final Class<? extends ClientProvider> client) {
+ clients.add(new ComponentItem<>(binding, client));
+ return this;
+ }
+
+ /**
+ * @param id server compoent id
+ * @param server the server class
+ * @return builder
+ */
+ public Container server(final String id, final Class<? extends ServerProvider> server) {
+ servers.add(new ComponentItem<>(id, server));
+ return this;
+ }
+
+ /**
+ * @param component make this component available to the container
+ * @return builder
+ */
+ public Container component(final Class<?> component) {
+ return component(component.getName(), component, (ConfigInstance) null);
+ }
+
+ /**
+ * @param component make this component available to the container
+ * @return builder
+ */
+ public Container component(String id, final Class<?> component, ConfigInstance... configs) {
+ components.add(new ComponentItem<>(id, component, configs));
+ return this;
+ }
+
+ /**
+ * @param config add this config to the application
+ * @return builder
+ */
+ public Container config(final ConfigInstance config) {
+ configs.add(config);
+ return this;
+ }
+
+ // generate services.xml based on this builder
+ private void build(PrintWriter xml, String id, int port) throws Exception {
+ xml.println("<jdisc version=\"1.0\" id=\"" + id + "\">");
+
+ if (port > 0) {
+ xml.println("<http>");
+ xml.println("<server id=\"http\" port=\"" + port + "\" />");
+ xml.println("</http>");
+ }
+
+ for (ComponentItem<? extends RequestHandler> entry : handlers) {
+ xml.println("<handler id=\"" + entry.component.getName() + "\">");
+ xml.println("<binding>" + entry.id + "</binding>");
+ xml.println("</handler>");
+ }
+
+ for (ComponentItem<? extends ClientProvider> entry : clients) {
+ xml.println("<client id=\"" + entry.component.getName() + "\">");
+ xml.println("<binding>" + entry.id + "</binding>");
+ xml.println("</client>");
+ }
+
+ for (ComponentItem<? extends ServerProvider> server : servers) {
+ generateComponent(xml, server, "server");
+ }
+
+ // container scoped configs
+ for (ConfigInstance config : configs) {
+ generateConfig(xml, config);
+ }
+
+ for (ComponentItem<?> component : components) {
+ generateComponent(xml, component, "component");
+ }
+
+ if (!docprocs.isEmpty()) {
+ xml.println("<document-processing>");
+ for (Map.Entry<String, List<ComponentItem<? extends DocumentProcessor>>> entry : docprocs.entrySet()) {
+ xml.println("<chain id=\"" + entry.getKey() + "\">");
+ for (ComponentItem<? extends DocumentProcessor> docproc : entry.getValue()) {
+ generateComponent(xml, docproc, "documentprocessor");
+ }
+ xml.println("</chain>");
+ }
+ xml.println("</document-processing>");
+ }
+
+ if (enableSearch || !searchers.isEmpty() || !renderers.isEmpty()) {
+ xml.println("<search>");
+ for (Map.Entry<String, List<ComponentItem<? extends Searcher>>> entry : searchers.entrySet()) {
+ xml.println("<chain id=\"" + entry.getKey() + "\">");
+ for (ComponentItem<? extends Searcher> searcher : entry.getValue()) {
+ generateComponent(xml, searcher, "searcher");
+ }
+ xml.println("</chain>");
+ }
+ for (ComponentItem<? extends Renderer> renderer : renderers) {
+ generateComponent(xml, renderer, "renderer");
+ }
+ xml.println("</search>");
+ }
+
+ xml.println("</jdisc>");
+ }
+
+ private void generateComponent(PrintWriter xml, ComponentItem<?> componentItem, String elementName) throws Exception {
+ xml.print("<" + elementName + " id=\"" + componentItem.id + "\" class=\"" + componentItem.component.getName() + "\"");
+ if (componentItem.configs.isEmpty() || (!componentItem.configs.isEmpty() && componentItem.configs.get(0) == null)) {
+ xml.println(" />");
+ } else {
+ xml.println(">");
+ for (ConfigInstance config : componentItem.configs) {
+ generateConfig(xml, config);
+ }
+ xml.println("</" + elementName + ">");
+ }
+ }
+
+ // uses reflection to generate XML from a config object
+ private void generateConfig(PrintWriter xml, ConfigInstance config) throws Exception {
+ Field nameField = config.getClass().getField("CONFIG_DEF_NAME");
+ String name = (String) nameField.get(config);
+ Field namespaceField = config.getClass().getField("CONFIG_DEF_NAMESPACE");
+ String namespace = (String) namespaceField.get(config);
+
+ xml.println("<config name=\"" + namespace + "." + name + "\">");
+ generateConfigNode(xml, config);
+ xml.println("</config>");
+ }
+
+ private void generateConfigNode(PrintWriter xml, InnerNode node) throws Exception {
+ // print all leaf nodes as config values
+ Field[] fields = node.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ generateConfigField(xml, node, field);
+ }
+ }
+
+ private void generateConfigField(PrintWriter xml, InnerNode node, Field field) throws Exception {
+ field.setAccessible(true);
+ if (LeafNode.class.isAssignableFrom(field.getType())) {
+ LeafNode<?> value = (LeafNode<?>) field.get(node);
+ if (value.value() != null) {
+ xml.print("<" + field.getName());
+ String v = value.getValue();
+ if (v.isEmpty()) {
+ xml.println(" />");
+ } else {
+ xml.println(">" + v + "</" + field.getName() + ">");
+ }
+ }
+ } else if (InnerNode.class.isAssignableFrom(field.getType())) {
+ xml.println("<" + field.getName() + ">");
+ generateConfigNode(xml, (InnerNode) field.get(node));
+ xml.println("</" + field.getName() + ">");
+ } else if (Map.class.isAssignableFrom(field.getType())) {
+ Map<?, ?> map = (Map<?, ?>) field.get(node);
+ if (!map.isEmpty()) {
+ xml.println("<" + field.getName() + ">");
+ for (Map.Entry<?, ?> entry : map.entrySet()) {
+ if (entry.getValue() instanceof InnerNode) {
+ xml.println("<item key=\"" + entry.getKey() + "\">");
+ generateConfigNode(xml, (InnerNode) entry.getValue());
+ xml.println("</item>");
+ } else if (entry.getValue() instanceof LeafNode) {
+ xml.println("<item key=\"" + entry.getKey() + "\">" + ((LeafNode<?>) entry.getValue()).getValue() + "</item>");
+ }
+ }
+ xml.println("</" + field.getName() + ">");
+ }
+ } else if (InnerNodeVector.class.isAssignableFrom(field.getType())) {
+ InnerNodeVector<? extends InnerNode> vector = (InnerNodeVector<? extends InnerNode>) field.get(node);
+ if (!vector.isEmpty()) {
+ xml.println("<" + field.getName() + ">");
+ for (InnerNode innerNode : vector) {
+ xml.println("<item>");
+ generateConfigNode(xml, innerNode);
+ xml.println("</item>");
+ }
+ xml.println("</" + field.getName() + ">");
+ }
+ } else if (LeafNodeVector.class.isAssignableFrom(field.getType())) {
+ LeafNodeVector<?, ? extends LeafNode<?>> vector = (LeafNodeVector<?, ? extends LeafNode<?>>) field.get(node);
+ if (!vector.isEmpty()) {
+ xml.println("<" + field.getName() + ">");
+ for (LeafNode<?> item : vector) {
+ xml.println("<item>" + item.getValue() + "</item>");
+ }
+ xml.println("</" + field.getName() + ">");
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file