summaryrefslogtreecommitdiffstats
path: root/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java
diff options
context:
space:
mode:
Diffstat (limited to 'config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java')
-rw-r--r--config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java772
1 files changed, 772 insertions, 0 deletions
diff --git a/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java
new file mode 100644
index 00000000000..21e7de8fa7b
--- /dev/null
+++ b/config-application-package/src/main/java/com/yahoo/config/model/application/provider/FilesApplicationPackage.java
@@ -0,0 +1,772 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.config.model.application.provider;
+
+import com.yahoo.config.application.ConfigDefinitionDir;
+import com.yahoo.config.application.Xml;
+import com.yahoo.config.application.XmlPreProcessor;
+import com.yahoo.config.application.api.ApplicationMetaData;
+import com.yahoo.config.application.api.ComponentInfo;
+import com.yahoo.config.application.api.DeployLogger;
+import com.yahoo.config.application.api.RuleConfigDeriver;
+import com.yahoo.config.application.api.UnparsedConfigDefinition;
+import com.yahoo.config.codegen.DefParser;
+import com.yahoo.config.application.api.ApplicationFile;
+import com.yahoo.config.application.api.ApplicationPackage;
+import com.yahoo.config.provision.Version;
+import com.yahoo.config.provision.Zone;
+import com.yahoo.path.Path;
+import com.yahoo.io.HexDump;
+import com.yahoo.io.IOUtils;
+import com.yahoo.io.reader.NamedReader;
+import com.yahoo.log.LogLevel;
+import com.yahoo.text.Utf8;
+import com.yahoo.vespa.config.ConfigDefinition;
+import com.yahoo.vespa.config.ConfigDefinitionBuilder;
+import com.yahoo.vespa.config.ConfigDefinitionKey;
+import com.yahoo.vespa.config.util.ConfigUtils;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.*;
+import java.net.JarURLConnection;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.util.*;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.io.IOUtils.readAll;
+import static com.yahoo.text.Lowercase.toLowerCase;
+
+
+/**
+ * Application package derived from local files, ie. during deploy.
+ * Construct using {@link com.yahoo.config.model.application.provider.FilesApplicationPackage#fromFile(java.io.File)} or
+ * {@link com.yahoo.config.model.application.provider.FilesApplicationPackage#fromFileWithDeployData(java.io.File, DeployData)}.
+ *
+ * @author vegardh
+ */
+public class FilesApplicationPackage implements ApplicationPackage {
+
+ private static final Logger log = Logger.getLogger(FilesApplicationPackage.class.getName());
+ private static final String META_FILE_NAME = ".applicationMetaData";
+
+ private final File appDir;
+ private final File preprocessedDir;
+ private final File configDefsDir;
+ private final AppSubDirs appSubDirs;
+ // NOTE: these directories exist in the original user app, but their locations are given in 'services.xml'
+ private final List<String> userIncludeDirs = new ArrayList<>();
+ private final ApplicationMetaData metaData;
+
+ /**
+ * Returns an application package object based on the given application dir
+ *
+ * @param appDir application package directory
+ * @return an Application package instance
+ */
+ public static FilesApplicationPackage fromFile(File appDir) {
+ return new Builder(appDir).preprocessedDir(new File(appDir, ".preprocessed")).build();
+ }
+
+ /** Creates package from a local directory, typically deploy app */
+ public static FilesApplicationPackage fromFileWithDeployData(File appDir, DeployData deployData) {
+ return new Builder(appDir).deployData(deployData).build();
+ }
+
+ /**
+ * Builder for {@link com.yahoo.config.model.application.provider.FilesApplicationPackage}. Use
+ * this to create instances in a flexible manner.
+ */
+ public static class Builder {
+ private final File appDir;
+ private Optional<File> preprocessedDir = Optional.empty();
+ private Optional<ApplicationMetaData> metaData = Optional.empty();
+
+ public Builder(File appDir) {
+ this.appDir = appDir;
+ }
+
+ public Builder preprocessedDir(File preprocessedDir) {
+ this.preprocessedDir = Optional.ofNullable(preprocessedDir);
+ return this;
+ }
+
+ public Builder deployData(DeployData deployData) {
+ this.metaData = Optional.of(metaDataFromDeployData(appDir, deployData));
+ return this;
+ }
+
+ public FilesApplicationPackage build() {
+ return new FilesApplicationPackage(appDir, preprocessedDir.orElse(new File(appDir, ".preprocessed")), metaData.orElse(readMetaData(appDir)));
+ }
+ }
+
+ private static ApplicationMetaData metaDataFromDeployData(File appDir, DeployData deployData) {
+ return new ApplicationMetaData(deployData.getDeployedByUser(), deployData.getDeployedFromDir(), deployData.getDeployTimestamp(),
+ deployData.getApplicationName(), computeCheckSum(appDir), deployData.getGeneration(), deployData.getCurrentlyActiveGeneration());
+ }
+
+ /**
+ * New package from given path on local file system. Retrieves config definition files from
+ * the default location 'serverdb/classes'.
+ *
+ * @param appDir application package directory
+ * @param preprocessedDir preprocessed application package output directory
+ * @param metaData metadata for this application package
+ */
+ @SuppressWarnings("deprecation")
+ private FilesApplicationPackage(File appDir, File preprocessedDir, ApplicationMetaData metaData) {
+ verifyAppDir(appDir);
+ this.appDir = appDir;
+ this.preprocessedDir = preprocessedDir;
+ appSubDirs = new AppSubDirs(appDir);
+ configDefsDir = new File(appDir, ApplicationPackage.CONFIG_DEFINITIONS_DIR);
+ addUserIncludeDirs();
+ this.metaData = metaData;
+ }
+
+ public String getApplicationName() {
+ return metaData.getApplicationName();
+ }
+
+ @Override
+ public List<NamedReader> getFiles(Path relativePath, String suffix, boolean recurse) {
+ return getFiles(relativePath, "", suffix, recurse);
+ }
+
+ @Override
+ public ApplicationFile getFile(Path path) {
+ File file = (path.isRoot() ? appDir : new File(appDir, path.getRelative()));
+ return new FilesApplicationFile(path, file);
+ }
+
+ @Override
+ public ApplicationMetaData getMetaData() {
+ return metaData;
+ }
+
+ private List<NamedReader> getFiles(Path relativePath,String namePrefix,String suffix,boolean recurse) {
+ try {
+ List<NamedReader> readers=new ArrayList<>();
+ File dir = new File(appDir, relativePath.getRelative());
+ if ( ! dir.isDirectory()) return readers;
+
+ final File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ if (recurse)
+ readers.addAll(getFiles(relativePath.append(file.getName()), namePrefix + "/" + file.getName(), suffix, recurse));
+ } else {
+ if (suffix == null || file.getName().endsWith(suffix))
+ readers.add(new NamedReader(file.getName(), new FileReader(file)));
+ }
+ }
+ }
+ return readers;
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Could not open (all) files in '" + relativePath + "'",e);
+ }
+ }
+
+ private void verifyAppDir(File appDir) {
+ if (appDir==null || !appDir.isDirectory()) {
+ throw new IllegalArgumentException("Path '" + appDir + "' is not a directory.");
+ }
+ if (! appDir.canRead()){
+ throw new IllegalArgumentException("Cannot read from application directory '" + appDir + "'");
+ }
+ }
+
+ @Override
+ public Reader getHosts() {
+ try {
+ File hostsFile = getHostsFile();
+ if (!hostsFile.exists()) return null;
+ return new FileReader(hostsFile);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ public String getHostSource() {
+ return getHostsFile().getPath();
+ }
+
+ @SuppressWarnings("deprecation")
+ private File getHostsFile() {
+ return new File(appDir, ApplicationPackage.HOSTS);
+ }
+
+ private File getFileWithFallback(String first, String second) {
+ File firstFile = new File(appDir, first);
+ File secondFile = new File(appDir, second);
+ if (firstFile.exists()) {
+ return firstFile;
+ } else if (secondFile.exists()) {
+ return secondFile;
+ } else {
+ return firstFile;
+ }
+ }
+
+ @Override
+ public String getServicesSource() {
+ return getServicesFile().getPath();
+ }
+
+ @SuppressWarnings("deprecation")
+ private File getServicesFile() {
+ return new File(appDir, ApplicationPackage.SERVICES);
+ }
+
+ @Override
+ public Optional<Reader> getDeployment() { return optionalFile(DEPLOYMENT_FILE); }
+
+ @Override
+ public Optional<Reader> getValidationOverrides() { return optionalFile(VALIDATION_OVERRIDES); }
+
+ private Optional<Reader> optionalFile(Path filePath) {
+ try {
+ return Optional.of(getFile(filePath).createReader());
+ } catch (FileNotFoundException e) {
+ return Optional.empty();
+ }
+ }
+
+
+ @Override
+ public List<String> getUserIncludeDirs() {
+ return Collections.unmodifiableList(userIncludeDirs);
+ }
+
+ public void addUserIncludeDirs() {
+ Document services;
+ try {
+ services = Xml.getDocument(getServices());
+ } catch (Exception e) {
+ return; // This method does not validate that services.xml exists, or that it is valid xml.
+ }
+ NodeList includeNodes = services.getElementsByTagName(IncludeDirs.INCLUDE);
+
+ for (int i=0; i < includeNodes.getLength(); i++) {
+ Node includeNode = includeNodes.item(i);
+ addIncludeDir(includeNode);
+ }
+ }
+
+ private void addIncludeDir(Node includeNode) {
+ if (! (includeNode instanceof Element))
+ return;
+ Element include = (Element) includeNode;
+ if (! include.hasAttribute(IncludeDirs.DIR))
+ return;
+ String dir = include.getAttribute(IncludeDirs.DIR);
+ validateIncludeDir(dir);
+ IncludeDirs.validateFilesInIncludedDir(dir, include.getParentNode(), this);
+ log.log(LogLevel.INFO, "Adding user include dir '" + dir + "'");
+ userIncludeDirs.add(dir);
+ }
+
+ @Override
+ public void validateIncludeDir(String dirName) {
+ IncludeDirs.validateIncludeDir(dirName, this);
+ }
+
+ @Override
+ public Collection<NamedReader> searchDefinitionContents() {
+ Map<String, NamedReader> ret = new LinkedHashMap<>();
+ Set<String> fileSds = new LinkedHashSet<>();
+ Set<String> bundleSds = new LinkedHashSet<>();
+ try {
+ for (File f : getSearchDefinitionFiles()) {
+ fileSds.add(f.getName());
+ ret.put(f.getName(), new NamedReader(f.getName(), new FileReader(f)));
+ }
+ for (Map.Entry<String, String> e : allSdsFromDocprocBundlesAndClasspath(appDir).entrySet()) {
+ bundleSds.add(e.getKey());
+ ret.put(e.getKey(), new NamedReader(e.getKey(), new StringReader(e.getValue())));
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Couldn't get search definition contents.", e);
+ }
+ verifySdsDisjoint(fileSds, bundleSds);
+ return ret.values();
+ }
+
+ /**
+ * Verify that two sets of search definitions are disjoint (TODO: everything except error message is very generic).
+ * @param fileSds Set of search definitions from file
+ * @param bundleSds Set of search definitions from bundles
+ */
+ private void verifySdsDisjoint(Set<String> fileSds, Set<String> bundleSds) {
+ if (!Collections.disjoint(fileSds, bundleSds)) {
+ Collection<String> disjoint = new ArrayList<>(fileSds);
+ disjoint.retainAll(bundleSds);
+ throw new IllegalArgumentException("For the following search definitions names there are collisions between those specified inside " +
+ "docproc bundles and those in searchdefinitions/ in application package: "+disjoint);
+ }
+ }
+
+ /**
+ * Returns sdName→payload for all SDs in all docproc bundles and on local classpath.
+ * Throws {@link IllegalArgumentException} if there are multiple sd files of same name.
+ * @param appDir application package directory
+ * @return a map from search definition name to search definition content
+ * @throws IOException if reading a search definition fails
+ */
+ public static Map<String, String> allSdsFromDocprocBundlesAndClasspath(File appDir) throws IOException {
+ File dpChains = new File(appDir, ApplicationPackage.COMPONENT_DIR);
+ if (!dpChains.exists() || !dpChains.isDirectory()) return Collections.emptyMap();
+ List<String> usedNames = new ArrayList<>();
+ Map<String, String> ret = new LinkedHashMap<>();
+
+ // try classpath first
+ allSdsOnClassPath(usedNames, ret);
+
+ for (File bundle : dpChains.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.endsWith(".jar");
+ }})) {
+ for(Map.Entry<String, String> entry : ApplicationPackage.getBundleSdFiles("", new JarFile(bundle)).entrySet()) {
+ String sdName = entry.getKey();
+ if (usedNames.contains(sdName)) {
+ throw new IllegalArgumentException("The search definition name '"+sdName+"' used in bundle '"+
+ bundle.getName()+"' is already used in classpath or previous bundle.");
+ }
+ usedNames.add(sdName);
+ String sdPayload = entry.getValue();
+ ret.put(sdName, sdPayload);
+ }
+ }
+ return ret;
+ }
+
+ private static void allSdsOnClassPath(List<String> usedNames, Map<String, String> ret) throws IOException {
+ Enumeration<java.net.URL> resources = FilesApplicationPackage.class.getClassLoader().getResources(ApplicationPackage.SEARCH_DEFINITIONS_DIR.getRelative());
+
+ while(resources.hasMoreElements()) {
+ URL resource = resources.nextElement();
+
+ String protocol = resource.getProtocol();
+
+ if ("file".equals(protocol)) {
+ File file;
+ try {
+ file = new File(resource.toURI());
+ } catch (URISyntaxException e) {
+ continue;
+ }
+ // only interested in directories
+ if (file.isDirectory()) {
+ List<File> sdFiles = getSearchDefinitionFiles(file);
+ for (File sdFile : sdFiles) {
+ String sdName = sdFile.getName();
+ if (usedNames.contains(sdName)) {
+ throw new IllegalArgumentException("The search definition name '"+sdName+
+ "' found in classpath already used earlier in classpath.");
+ }
+ usedNames.add(sdName);
+ String contents = IOUtils.readAll(new FileReader(sdFile));
+ ret.put(sdFile.getName(), contents);
+ }
+ }
+ }
+ else if ("jar".equals(protocol)) {
+ JarURLConnection jarConnection = (JarURLConnection) resource.openConnection();
+ JarFile jarFile = jarConnection.getJarFile();
+ for(Map.Entry<String, String> entry : ApplicationPackage.getBundleSdFiles("", jarFile).entrySet()) {
+ String sdName = entry.getKey();
+ if (usedNames.contains(sdName)) {
+ throw new IllegalArgumentException("The search definitions name '"+sdName+
+ "' used in bundle '"+jarFile.getName()+"' already used in classpath or previous bundle.");
+ }
+ usedNames.add(sdName);
+ String sdPayload = entry.getValue();
+ ret.put(sdName, sdPayload);
+ }
+ }
+ }
+ }
+
+ private Reader retrieveConfigDefReader(String defName) {
+ File def = new File(configDefsDir + File.separator + defName);
+ try {
+ return new NamedReader(def.getAbsolutePath(), new FileReader(def));
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Could not read config definition file '" +
+ def.getAbsolutePath() + "'", e);
+ }
+ }
+
+ @Override
+ public Map<ConfigDefinitionKey, UnparsedConfigDefinition> getAllExistingConfigDefs() {
+ Map<ConfigDefinitionKey, UnparsedConfigDefinition> defs = new LinkedHashMap<>();
+
+ if (configDefsDir.isDirectory()) {
+ addAllDefsFromConfigDir(defs, configDefsDir);
+ }
+ addAllDefsFromBundles(defs, FilesApplicationPackage.getComponents(appDir));
+ return defs;
+ }
+
+ private void addAllDefsFromBundles(Map<ConfigDefinitionKey, UnparsedConfigDefinition> defs, List<Component> components) {
+ for (Component component : components) {
+ Bundle bundle = component.getBundle();
+ for (final Bundle.DefEntry def : bundle.getDefEntries()) {
+ final ConfigDefinitionKey defKey = new ConfigDefinitionKey(def.defName, def.defNamespace);
+ if (!defs.containsKey(defKey)) {
+ defs.put(defKey, new UnparsedConfigDefinition() {
+ @Override
+ public ConfigDefinition parse() {
+ DefParser parser = new DefParser(defKey.getName(), new StringReader(def.contents));
+ return ConfigDefinitionBuilder.createConfigDefinition(parser.getTree());
+ }
+
+ @Override
+ public String getUnparsedContent() {
+ return def.contents;
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private void addAllDefsFromConfigDir(Map<ConfigDefinitionKey, UnparsedConfigDefinition> defs, File configDefsDir) {
+ log.log(LogLevel.DEBUG, "Getting all config definitions from '" + configDefsDir + "'");
+ for (final File def : configDefsDir.listFiles(
+ new FilenameFilter() { @Override public boolean accept(File dir, String name) {
+ return name.matches(".*\\.def");}})) {
+
+ log.log(LogLevel.DEBUG, "Processing config definition '" + def + "'");
+ String[] nv = def.getName().split("\\.def");
+ if (nv == null) {
+ log.log(LogLevel.WARNING, "Skipping '" + def + "', cannot determine name");
+ } else {
+ ConfigDefinitionKey key;
+ try {
+ key = ConfigUtils.createConfigDefinitionKeyFromDefFile(def);
+ } catch (IOException e) {
+ e.printStackTrace();
+ break;
+ }
+ if (key.getNamespace().isEmpty()) {
+ throw new IllegalArgumentException("Config definition '" + nv + "' has no namespace");
+ }
+ boolean addFile = false;
+ if (defs.containsKey(key)) {
+ if (nv[0].contains(".")) {
+ log.log(LogLevel.INFO, "Two config definitions found for the same name and namespace: " + key + ". The file '" + def + "' will take precedence");
+ addFile = true;
+ } else {
+ log.log(LogLevel.INFO, "Two config definitions found for the same name and namespace: " + key + ". Skipping '" + def + "', as it does not contain namespace in filename");
+ }
+ } else {
+ addFile = true;
+ }
+ if (addFile) {
+ log.log(LogLevel.DEBUG, "Adding " + key + " to archive of all existing config defs");
+ final ConfigDefinitionKey finalKey = key;
+ defs.put(key, new UnparsedConfigDefinition() {
+ @Override
+ public ConfigDefinition parse() {
+ DefParser parser = new DefParser(finalKey.getName(), retrieveConfigDefReader(def.getName()));
+ return ConfigDefinitionBuilder.createConfigDefinition(parser.getTree());
+ }
+
+ @Override
+ public String getUnparsedContent() {
+ return readConfigDefinition(def.getName());
+ }
+ });
+ }
+ }
+ }
+ }
+
+ private String readConfigDefinition(String name) {
+ try (Reader reader = retrieveConfigDefReader(name)) {
+ return IOUtils.readAll(reader);
+ } catch (IOException e) {
+ throw new RuntimeException("Error reading config definition " + name, e);
+ }
+ }
+
+ @Override
+ public Reader getServices() {
+ try {
+ return new FileReader(getServicesSource());
+ } catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ //Only intended for DeployProcessor, others should use the member version
+ static List<File> getSearchDefinitionFiles(File appDir) {
+ //The dot is escaped later in this method:
+ assert (ApplicationPackage.SD_NAME_SUFFIX.charAt(0) == '.');
+
+ List<File> ret = new ArrayList<>();
+ File sdDir;
+
+ sdDir = new File(appDir, ApplicationPackage.SEARCH_DEFINITIONS_DIR.getRelative());
+ if (!sdDir.isDirectory()) {
+ return ret;
+ }
+ ret.addAll(Arrays.asList(
+ sdDir.listFiles(
+ new FilenameFilter() { @Override public boolean accept(File dir, String name) {
+ return name.matches(".*\\" + ApplicationPackage.SD_NAME_SUFFIX);}})));
+ return ret;
+ }
+
+ public List<File> getSearchDefinitionFiles() {
+ return getSearchDefinitionFiles(appDir);
+ }
+
+ //Only for use by deploy processor
+ public static List<Component> getComponents(File appDir) {
+ List<Component> components = new ArrayList<>();
+ for (Bundle bundle : Bundle.getBundles(new File(appDir, ApplicationPackage.COMPONENT_DIR))) {
+ components.add(new Component(bundle, new ComponentInfo(new File(ApplicationPackage.COMPONENT_DIR, bundle.getFile().getName()).getPath())));
+ }
+ return components;
+ }
+
+ private static List<ComponentInfo> getComponentsInfo(File appDir) {
+ List<ComponentInfo> components = new ArrayList<>();
+ for (Bundle bundle : Bundle.getBundles(new File(appDir, ApplicationPackage.COMPONENT_DIR))) {
+ components.add(new ComponentInfo(new File(ApplicationPackage.COMPONENT_DIR, bundle.getFile().getName()).getPath()));
+ }
+ return components;
+ }
+
+ @Override
+ public List<ComponentInfo> getComponentsInfo(Version vespaVersion) {
+ return getComponentsInfo(appDir);
+ }
+
+ /**
+ * Returns a list of all components in this package.
+ *
+ * @return A list of components.
+ */
+ public List<Component> getComponents() {
+ return getComponents(appDir);
+ }
+
+ public File getAppDir() throws IOException {
+ return appDir.getCanonicalFile();
+ }
+
+ public static ApplicationMetaData readMetaData(File appDir) {
+ ApplicationMetaData defaultMetaData = new ApplicationMetaData(appDir, "n/a", "n/a", 0l, "", 0l, 0l);
+ File metaFile = new File(appDir, META_FILE_NAME);
+ if (!metaFile.exists()) {
+ return defaultMetaData;
+ }
+ try (FileReader reader = new FileReader(metaFile)) {
+ return ApplicationMetaData.fromJsonString(IOUtils.readAll(reader));
+ } catch (Exception e) {
+ // Not a big deal, return default
+ return defaultMetaData;
+ }
+ }
+
+ /**
+ * Represents a component in the application package. Immutable.
+ */
+ public static class Component {
+
+ public final ComponentInfo info;
+ private final Bundle bundle;
+
+ public Component(Bundle bundle, ComponentInfo info) {
+ this.bundle = bundle;
+ this.info = info;
+ }
+
+ public List<Bundle.DefEntry> getDefEntries() {
+ return bundle.getDefEntries();
+ }
+
+ public Bundle getBundle() {
+ return bundle;
+ }
+ } // class Component
+
+ /**
+ * Reads a ranking expression from file to a string and returns it.
+ *
+ * @param name the name of the file to return, either absolute or
+ * relative to the search definition directory in the application package
+ * @return the content of a ranking expression file
+ * @throws IllegalArgumentException if the file was not found or could not be read
+ */
+ // TODO: A note on absolute paths: We don't want to support this and it should be removed on 6.0
+ // Currently one system test (basicmlr) depends on it.
+ @Override
+ public Reader getRankingExpression(String name) {
+ try {
+ return IOUtils.createReader(expressionFileNameToFile(name), "utf-8");
+ }
+ catch (IOException e) {
+ throw new IllegalArgumentException("Could not read ranking expression file '" + name + "'", e);
+ }
+ }
+
+ private File expressionFileNameToFile(String name) {
+ File expressionFile = new File(name);
+ if (expressionFile.isAbsolute()) return expressionFile;
+
+ File sdDir = new File(appDir, ApplicationPackage.SEARCH_DEFINITIONS_DIR.getRelative());
+ return new File(sdDir, name);
+ }
+
+ @Override
+ public File getFileReference(Path pathRelativeToAppDir) {
+ return new File(appDir, pathRelativeToAppDir.getRelative());
+ }
+
+ @Override
+ public void validateXML(DeployLogger logger) throws IOException {
+ validateXML(logger, Optional.empty());
+ }
+
+ @Override
+ public void validateXML(DeployLogger logger, Optional<Version> vespaVersion) throws IOException {
+ ApplicationPackageXmlFilesValidator xmlFilesValidator = ApplicationPackageXmlFilesValidator.createDefaultXMLValidator(appDir, logger, vespaVersion);
+ xmlFilesValidator.checkApplication();
+ ApplicationPackageXmlFilesValidator.checkIncludedDirs(this);
+ }
+
+ @Override
+ public void writeMetaData() throws IOException {
+ File metaFile = new File(appDir, META_FILE_NAME);
+ IOUtils.writeFile(metaFile, metaData.asJsonString(), false);
+ }
+
+ @Override
+ public Collection<NamedReader> getSearchDefinitions() {
+ return searchDefinitionContents();
+ }
+
+ private void preprocessXML(File destination, File inputXml, Zone zone) throws ParserConfigurationException, TransformerException, SAXException, IOException {
+ Document document = new XmlPreProcessor(appDir, inputXml, zone.environment(), zone.region()).run();
+ Transformer transformer = TransformerFactory.newInstance().newTransformer();
+ try (FileOutputStream outputStream = new FileOutputStream(destination)) {
+ transformer.transform(new DOMSource(document), new StreamResult(outputStream));
+ }
+ }
+
+ @Override
+ public ApplicationPackage preprocess(Zone zone, RuleConfigDeriver ignored, DeployLogger logger) throws IOException, TransformerException, ParserConfigurationException, SAXException {
+ IOUtils.recursiveDeleteDir(preprocessedDir);
+ IOUtils.copyDirectory(appDir, preprocessedDir, -1, (dir, name) -> !name.equals(".preprocessed") &&
+ !name.equals(SERVICES) &&
+ !name.equals(HOSTS) &&
+ !name.equals(CONFIG_DEFINITIONS_DIR));
+ preprocessXML(new File(preprocessedDir, SERVICES), getServicesFile(), zone);
+ if (getHostsFile().exists()) {
+ preprocessXML(new File(preprocessedDir, HOSTS), getHostsFile(), zone);
+ }
+ FilesApplicationPackage preprocessed = FilesApplicationPackage.fromFile(preprocessedDir);
+ preprocessed.copyUserDefsIntoApplication();
+ return preprocessed;
+ }
+
+ private void copyUserDefsIntoApplication() {
+ File destination = appSubDirs.configDefs();
+ destination.mkdir();
+ ConfigDefinitionDir defDir = new ConfigDefinitionDir(destination);
+ // Copy the user's def files from components.
+ List<Bundle> bundlesAdded = new ArrayList<>();
+ for (FilesApplicationPackage.Component component : FilesApplicationPackage.getComponents(appSubDirs.root())) {
+ Bundle bundle = component.getBundle();
+ defDir.addConfigDefinitionsFromBundle(bundle, bundlesAdded);
+ bundlesAdded.add(bundle);
+ }
+ }
+
+ /**
+ * Computes an md5 hash of the contents of the application package
+ *
+ * @return an md5sum of the application package
+ */
+ private static String computeCheckSum(File appDir) {
+ MessageDigest md5;
+ try {
+ md5 = MessageDigest.getInstance("MD5");
+ for (File file : appDir.listFiles((dir, name) -> !name.equals(ApplicationPackage.EXT_DIR) && !name.startsWith("."))) {
+ addPathToDigest(file, "", md5, true, false);
+ }
+ return toLowerCase(HexDump.toHexString(md5.digest()));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ /**
+ * Adds the given path to the digest, or does nothing if path is neither file nor dir
+ * @param path path to add to message digest
+ * @param suffix only files with this suffix are considered
+ * @param digest the {link @MessageDigest} to add the file paths to
+ * @param recursive whether to recursively find children in the paths
+ * @param fullPathNames Whether to include the full paths in checksum or only the names
+ * @throws java.io.IOException if adding path to digest fails when reading files from path
+ */
+ private static void addPathToDigest(File path, String suffix, MessageDigest digest, boolean recursive, boolean fullPathNames) throws IOException {
+ if (!path.exists()) return;
+ if (fullPathNames) {
+ digest.update(path.getPath().getBytes(Utf8.getCharset()));
+ } else {
+ digest.update(path.getName().getBytes(Utf8.getCharset()));
+ }
+ if (path.isFile()) {
+ FileInputStream is = new FileInputStream(path);
+ addToDigest(is, digest);
+ is.close();
+ } else if (path.isDirectory()) {
+ final File[] files = path.listFiles();
+ if (files != null) {
+ for (File elem : files) {
+ if ((elem.isDirectory() && recursive) || elem.getName().endsWith(suffix)) {
+ addPathToDigest(elem, suffix, digest, recursive, fullPathNames);
+ }
+ }
+ }
+ }
+ }
+
+ private static final int MD5_BUFFER_SIZE = 65536;
+ private static void addToDigest(InputStream is, MessageDigest digest) throws IOException {
+ if (is==null) return;
+ byte[] buffer = new byte[MD5_BUFFER_SIZE];
+ int i;
+ do {
+ i=is.read(buffer);
+ if (i > 0) {
+ digest.update(buffer, 0, i);
+ }
+ } while(i!=-1);
+ }
+
+}