/*******************************************************************************
* Copyright (c) 2014, 2015 IBM Corporation and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.orion.server.cf.manifest.v2.utils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileInfo;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.orion.server.cf.manifest.v2.Analyzer;
import org.eclipse.orion.server.cf.manifest.v2.AnalyzerException;
import org.eclipse.orion.server.cf.manifest.v2.InvalidAccessException;
import org.eclipse.orion.server.cf.manifest.v2.ManifestParseTree;
import org.eclipse.orion.server.cf.manifest.v2.ParserException;
import org.eclipse.osgi.util.NLS;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class ManifestUtils {
private static final Pattern NON_SLUG_PATTERN = Pattern.compile("[^\\w-]"); //$NON-NLS-1$
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\s]"); //$NON-NLS-1$
/* global defaults */
public static final String DEFAULT_MEMORY = "512M"; //$NON-NLS-1$
public static final String DEFAULT_INSTANCES = "1"; //$NON-NLS-1$
public static final String DEFAULT_PATH = "."; //$NON-NLS-1$
public static final String[] RESERVED_PROPERTIES = {//
"env", // //$NON-NLS-1$
"inherit", // //$NON-NLS-1$
"applications" // //$NON-NLS-1$
};
public static boolean isReserved(ManifestParseTree node) {
String value = node.getLabel();
for (String property : RESERVED_PROPERTIES)
if (property.equals(value))
return true;
return false;
}
public static final String[] APPLICATION_PROPERTIES = {//
"name", "memory", "host", "buildpack", "command", // //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$//$NON-NLS-4$ //$NON-NLS-5$
"domain", "instances", "path", "timeout", "no-route", "services"// //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
};
public static boolean isApplicationProperty(ManifestParseTree node) {
String value = node.getLabel();
for (String property : APPLICATION_PROPERTIES)
if (property.equals(value))
return true;
return false;
}
/**
* Inner helper method parsing single manifests with additional semantic analysis.
*/
protected static ManifestParseTree parseManifest(InputStream inputStream, String targetBase, Analyzer analyzer) throws IOException, ParserException, AnalyzerException {
/* run parser */
ManifestParser parser = new ManifestParser();
ManifestParseTree parseTree = parser.parse(inputStream);
/* perform inheritance transformations */
ManifestTransformator transformator = new ManifestTransformator();
transformator.apply(parseTree);
/* resolve symbols */
SymbolResolver symbolResolver = new SymbolResolver(targetBase);
symbolResolver.apply(parseTree);
/* validate common field values */
Analyzer applicationAnalyzer = analyzer != null ? analyzer : new ApplicationSanizator();
applicationAnalyzer.apply(parseTree);
return parseTree;
}
/**
* Inner helper method parsing single manifests with additional semantic analysis.
*/
protected static ManifestParseTree parseManifest(IFileStore manifestFileStore, String targetBase, Analyzer analyzer) throws CoreException, IOException, ParserException, AnalyzerException {
/* basic sanity checks */
IFileInfo manifestFileInfo = manifestFileStore.fetchInfo();
if (!manifestFileInfo.exists() || manifestFileInfo.isDirectory())
throw new IOException(ManifestConstants.MISSING_OR_INVALID_MANIFEST);
if (manifestFileInfo.getLength() == EFS.NONE)
throw new IOException(ManifestConstants.EMPTY_MANIFEST);
if (manifestFileInfo.getLength() > ManifestConstants.MANIFEST_SIZE_LIMIT)
throw new IOException(ManifestConstants.MANIFEST_FILE_SIZE_EXCEEDED);
InputStream inputStream = manifestFileStore.openInputStream(EFS.NONE, null);
ManifestParseTree manifestTree = null;
try {
manifestTree = parseManifest(inputStream, targetBase, analyzer);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return manifestTree;
}
/**
* Utility method wrapping manifest parse process including inheritance and additional semantic analysis.
* @param sandbox The file store used to limit manifest inheritance, i.e. each parent manifest has to be a
* transitive child of the sandbox.
* @param manifestStore Manifest file store used to fetch the manifest contents.
* @param targetBase Cloud foundry target base used to resolve manifest symbols.
* @param manifestList List of forbidden manifest paths considered in the recursive inheritance process.
* Used to detect inheritance cycles.
* @return An intermediate manifest tree representation.
* @throws CoreException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
* @throws InvalidAccessException
*/
public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase, Analyzer analyzer, List<IPath> manifestList) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
ManifestParseTree manifest = parseManifest(manifestStore, targetBase, analyzer);
if (!manifest.has(ManifestConstants.INHERIT))
/* nothing to do */
return manifest;
/* check if the parent manifest is within the given sandbox */
IPath parentLocation = new Path(manifest.get(ManifestConstants.INHERIT).getValue());
if (!InheritanceUtils.isWithinSandbox(sandbox, manifestStore, parentLocation))
throw new AnalyzerException(NLS.bind(ManifestConstants.FORBIDDEN_ACCESS_ERROR, manifest.get(ManifestConstants.INHERIT).getValue()));
/* detect inheritance cycles */
if (manifestList.contains(parentLocation))
throw new AnalyzerException(ManifestConstants.INHERITANCE_CYCLE_ERROR);
manifestList.add(parentLocation);
IFileStore parentStore = manifestStore.getParent().getFileStore(parentLocation);
ManifestParseTree parentManifest = parse(sandbox, parentStore, targetBase, analyzer, manifestList);
InheritanceUtils.inherit(parentManifest, manifest);
/* perform additional inheritance transformations */
ManifestTransformator transformator = new ManifestTransformator();
transformator.apply(manifest);
return manifest;
}
/**
* Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
* @param sandbox
* @param manifestStore
* @return
* @throws CoreException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
* @throws InvalidAccessException
*/
public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
return parse(sandbox, manifestStore, null, null, new ArrayList<IPath>());
}
/**
* Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
* @param sandbox
* @param manifestStore
* @param targetBase
* @return
* @throws CoreException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
* @throws InvalidAccessException
*/
public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
return parse(sandbox, manifestStore, targetBase, null, new ArrayList<IPath>());
}
/**
* Helper method for {@link #parse(IFileStore, IFileStore, String, List<IPath>)}
* @param sandbox
* @param manifestStore
* @param targetBase
* @return
* @throws CoreException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
* @throws InvalidAccessException
*/
public static ManifestParseTree parse(IFileStore sandbox, IFileStore manifestStore, String targetBase, Analyzer analyzer) throws CoreException, IOException, ParserException, AnalyzerException, InvalidAccessException {
return parse(sandbox, manifestStore, targetBase, analyzer, new ArrayList<IPath>());
}
/**
* Normalizes the string memory measurement to a MB integer value.
* @param memory Manifest memory measurement.
* @return Normalized MB integer value.
*/
public static int normalizeMemoryMeasure(String memory) {
if (memory.toLowerCase().endsWith("m")) //$NON-NLS-1$
return Integer.parseInt(memory.substring(0, memory.length() - 1));
if (memory.toLowerCase().endsWith("mb")) //$NON-NLS-1$
return Integer.parseInt(memory.substring(0, memory.length() - 2));
if (memory.toLowerCase().endsWith("g")) //$NON-NLS-1$
return (1024 * Integer.parseInt(memory.substring(0, memory.length() - 1)));
if (memory.toLowerCase().endsWith("gb")) //$NON-NLS-1$
return (1024 * Integer.parseInt(memory.substring(0, memory.length() - 2)));
/* return default memory value, i.e. 1024 MB */
return 1024;
}
/**
* Slugifies the given input to be reusable as URL pattern.
* @param input Input to be slugified.
* @return Slugified input
*/
public static String slugify(String input) {
input = WHITESPACE_PATTERN.matcher(input).replaceAll(""); //$NON-NLS-1$
return NON_SLUG_PATTERN.matcher(input).replaceAll(""); //$NON-NLS-1$
}
/**
* Parses a manifest from the given JSON representation.
* Note: no cross-manifest inheritance is allowed.
* @param manifestJSON
* @return
* @throws IllegalArgumentException
* @throws JSONException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
*/
public static ManifestParseTree parse(JSONObject manifestJSON) throws IllegalArgumentException, JSONException, IOException, ParserException, AnalyzerException {
StringBuilder sb = new StringBuilder();
sb.append("---").append(System.getProperty("line.separator")); //$NON-NLS-1$ //$NON-NLS-2$
append(sb, manifestJSON, 0, false);
String manifestYAML = sb.toString();
InputStream inputStream = new ByteArrayInputStream(manifestYAML.getBytes("UTF-8")); //$NON-NLS-1$
ManifestParseTree manifestTree = null;
try {
manifestTree = parseManifest(inputStream, null, null);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return manifestTree;
}
private static void appendIndentation(StringBuilder sb, int indentation) {
/* print indentation */
for (int i = 0; i < indentation; ++i)
sb.append(" "); //$NON-NLS-1$
}
private static void append(StringBuilder sb, JSONArray arr, int indentation) throws JSONException {
for (int i = 0; i < arr.length(); ++i) {
appendIndentation(sb, indentation);
sb.append("-").append(" "); //$NON-NLS-1$ //$NON-NLS-2$
Object val = arr.get(i);
if (val instanceof String) {
sb.append((String) val);
sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
} else if (val instanceof JSONObject) {
JSONObject objVal = (JSONObject) val;
append(sb, objVal, indentation + 2, false);
} else
throw new IllegalArgumentException("Arrays may contain only JSON objects or string literals.");
}
}
private static void append(StringBuilder sb, JSONObject obj, int indentation, boolean indentFirst) throws JSONException {
String[] names = JSONObject.getNames(obj);
if (names == null) {
return;
}
for (int i = 0; i < names.length; ++i) {
String prop = names[i];
if (i != 0 || indentFirst)
appendIndentation(sb, indentation);
sb.append(prop).append(":"); //$NON-NLS-1$
Object val = obj.get(prop);
if (val instanceof String) {
sb.append(" ").append((String) val); //$NON-NLS-1$
sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
} else if (val instanceof Boolean) {
sb.append(" ").append(val.toString()); //$NON-NLS-1$
sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
} else if (val instanceof JSONObject) {
JSONObject objVal = (JSONObject) val;
sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
append(sb, objVal, indentation + 2, true);
} else if (val instanceof JSONArray) {
JSONArray arr = (JSONArray) val;
sb.append(System.getProperty("line.separator")); //$NON-NLS-1$
append(sb, arr, indentation);
} else
throw new IllegalArgumentException("Objects may contain only JSON objects, arrays or string literals.");
}
}
/**
* Creates a manifest boilerplate consisting of one application with the given name.
* @param applicationName
* @return
* @throws IllegalArgumentException
* @throws JSONException
* @throws IOException
* @throws TokenizerException
* @throws ParserException
* @throws AnalyzerException
*/
public static ManifestParseTree createBoilerplate(String applicationName) throws IllegalArgumentException, JSONException, IOException, ParserException, AnalyzerException {
JSONObject application = new JSONObject();
application.put(ManifestConstants.NAME, applicationName);
JSONArray applications = new JSONArray();
applications.put(application);
JSONObject manifest = new JSONObject();
manifest.put(ManifestConstants.APPLICATIONS, applications);
return parse(manifest);
}
/**
* Helper method for deciding whether a manifest has multiple applications or not.
* @param manifest
* @return
*/
public static boolean hasMultipleApplications(ManifestParseTree manifest) {
if (!manifest.has(ManifestConstants.APPLICATIONS))
return false;
ManifestParseTree applications = manifest.getOpt(ManifestConstants.APPLICATIONS);
if (!applications.isList())
return false;
return applications.getChildren().size() > 1;
}
/**
* Instruments application properties by copying values from the instrumentation JSON.
* Note, that this method will perform a shallow instrumentation of single string properties.
* @param manifest
* @param instrumentation
* @throws JSONException
* @throws InvalidAccessException
*/
public static void instrumentManifest(ManifestParseTree manifest, JSONObject instrumentation) throws JSONException, InvalidAccessException {
if (instrumentation == null || !manifest.has(ManifestConstants.APPLICATIONS))
return;
List<ManifestParseTree> applications = manifest.get(ManifestConstants.APPLICATIONS).getChildren();
if (instrumentation == null || instrumentation.length() == 0)
return;
for (String key : JSONObject.getNames(instrumentation)) {
Object value = instrumentation.get(key);
for (ManifestParseTree application : applications) {
if (ManifestConstants.MEMORY.equals(key) && !updateMemory(application, (String) value))
continue;
if (value instanceof String) {
application.put(key, (String) value);
} else if (value instanceof JSONObject) {
application.put(key, (JSONObject) value);
}
}
}
}
private static boolean updateMemory(ManifestParseTree application, String value) {
if (!application.has(ManifestConstants.MEMORY))
return true;
try {
String appMemoryString = application.get(ManifestConstants.MEMORY).getValue();
int appMemory = normalizeMemoryMeasure(appMemoryString);
int instrumentationMemory = normalizeMemoryMeasure(value);
return instrumentationMemory > appMemory;
} catch (InvalidAccessException e) {
return true;
}
}
}