blob: 1df30e89295affa7fea1b7f494ed75a55a343061 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.uima.ducc.cli;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.uima.UIMAFramework;
import org.apache.uima.ducc.common.IDuccUser;
import org.apache.uima.ducc.common.TcpStreamHandler;
import org.apache.uima.ducc.common.utils.DuccPropertiesResolver;
import org.apache.uima.ducc.transport.event.sm.IService.ServiceType;
import org.apache.uima.ducc.user.common.PrivateClassLoader;
import org.apache.uima.ducc.user.common.QuotedOptions;
import org.apache.uima.ducc.user.common.UimaUtils;
import org.apache.uima.util.Level;
import org.apache.uima.util.XMLInputSource;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
public class DuccUiUtilities {
private static final String DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
private static final String LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
public static String getUser() {
String user = System.getProperty("user.name");
String runmode = DuccPropertiesResolver.get(DuccPropertiesResolver.ducc_runmode);
if(runmode != null) {
if(runmode.equals("Test")) {
String envUser = System.getenv(IDuccUser.EnvironmentVariable.USER.value());
if(envUser != null) {
user = envUser;
}
}
}
return user;
}
public static String fixupEnvironment(String environment, String allInOne, String logDirectory) {
ArrayList<String> envList = QuotedOptions.tokenizeList(environment, false); // Don't strip quotes
Map<String, String> envMap = QuotedOptions.parseAssignments(envList, +1); // Expand any FOO or FOO* entries
// Rename the user's LD_LIBRARY_PATH as Secure Linuxs will not pass that on
// But not for --all-in-one local
if (allInOne == null || !allInOne.equalsIgnoreCase("local")) {
String source = "LD_LIBRARY_PATH";
String target = "DUCC_" + source;
if (envMap.containsKey(source)) {
if (!envMap.containsKey(target)) {
envMap.put(target, envMap.get(source));
envMap.remove(source);
}
}
}
// Augment user-specified environment with a few useful ones (only if not already set), e.g. USER HOME
String envNames = DuccPropertiesResolver.get(DuccPropertiesResolver.ducc_environment_propagated);
if (envNames != null) {
for (String name : envNames.split("\\s+")) {
if (!envMap.containsKey(name)) {
String value = QuotedOptions.quoteValue(name); // Quote value if necessary
if (value != null) {
envMap.put(name, value);
}
}
}
}
/*
* If an explicit DUCC_UMASK not provided include it with the current umask value so
* files created by this request inherit the current permissions.
* Get the current value by creating a temporary directory in the log directory
* and noting what permissions it doesn't have. UIMA-5328
*/
if (!envMap.containsKey("DUCC_UMASK")) {
File f = new File(logDirectory);
Path logDir = f.toPath();
try {
FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxrwxrwx"));
Path dir = Files.createTempDirectory(logDir, "ducc_umask", attr );
Set<PosixFilePermission> perms = Files.getFileAttributeView(dir, PosixFileAttributeView.class).readAttributes().permissions();
Files.delete(dir);
int umask = 777;
int[] bitVals = { 400, 200, 100, 40, 20, 10, 4, 2, 1 }; // Enums are in the usual ugo/rwx order
for (PosixFilePermission perm : perms) {
umask -= bitVals[perm.ordinal()];
}
envMap.put("DUCC_UMASK", String.format("%03d", umask));
} catch (IOException e) {
e.printStackTrace();
throw new IllegalArgumentException("fixupEnvironment: failed to create temporary directory in log_directory - " + e);
}
}
// Must rebuild the string ... note that quotes were preserved so can recreate easily
StringBuilder sb = new StringBuilder();
for (String name : envMap.keySet()) {
sb.append(name).append("=").append(envMap.get(name)).append(" ");
}
return sb.toString();
}
/*
* Get URL for service handling request. Either "orchestrator" or "sm"
*/
public static String dispatchUrl(String server) {
String host = DuccPropertiesResolver.get("ducc." + server + ".http.node");
String port = DuccPropertiesResolver.get("ducc." + server + ".http.port");
if ( host == null || port == null) {
throw new IllegalStateException("ducc." + server + ".http.node and/or .port not set in ducc.properties");
}
return "http://" + host + ":" + port + "/" + server.substring(0, 2);
}
private static void secureDocumentBuilderFactory(DocumentBuilderFactory documentBuilderFactory) {
try {
documentBuilderFactory.setFeature(DISALLOW_DOCTYPE_DECL, true);
} catch (ParserConfigurationException e1) {
UIMAFramework.getLogger().log(Level.WARNING,
"DocumentBuilderFactory didn't recognize setting feature " + DISALLOW_DOCTYPE_DECL);
}
try {
documentBuilderFactory.setFeature(LOAD_EXTERNAL_DTD, false);
} catch (ParserConfigurationException e) {
UIMAFramework.getLogger().log(Level.WARNING,
"DocumentBuilderFactory doesn't support feature " + LOAD_EXTERNAL_DTD);
}
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
}
/**
* Extract the endpoint from the deployment descriptor, resolving names and placeholders against
* the same environment as that of the JVM that will deploy the service
*
* @param working_dir
* @param process_DD
* @param jvmargs
* @return
*/
public static String getEndpoint(String working_dir, String process_DD, String jvmargs, String classpath) {
// convert relative path for process_DD to absolute if needed
if (!process_DD.startsWith("/") && process_DD.endsWith(".xml") && working_dir != null) {
process_DD = working_dir + "/" + process_DD;
}
// parse process_DD into DOM, resolving the descriptor either by name or by location
Document doc = null;
XMLInputSource xmlin;
try {
if (!process_DD.endsWith(".xml") && classpath != null) {
URLClassLoader classLoader = PrivateClassLoader.create(classpath);
xmlin = UimaUtils.getXMLInputSource(process_DD, classLoader);
} else {
xmlin = UimaUtils.getXMLInputSource(process_DD);
}
DocumentBuilder db = null;
DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
secureDocumentBuilderFactory(f);
db = f.newDocumentBuilder();
doc = db.parse(xmlin.getInputStream());
} catch (Throwable t) {
t.printStackTrace();
throw new IllegalArgumentException(t.getMessage());
}
// locate the <inputQueue node within the xml - should only be one such node, and it MUST exist
// then construct an endpoint and resolve any placeholders against the process JVM args
// just as is done by Spring in a UIMA-AS Deployment Descriptor
// Ignore any decorations on the broker URL as they are not part of the service name
NodeList nodes = doc.getElementsByTagName("inputQueue");
if (nodes.getLength() > 0) {
Element element = (Element) nodes.item(0);
String endpoint = element.getAttribute("endpoint");
String broker = element.getAttribute("brokerURL");
if (endpoint.contains("${") || broker.contains("${")) {
ArrayList<String> jvmargList = QuotedOptions.tokenizeList(jvmargs, true); // Strip quotes
Map<String, String> jvmargMap = QuotedOptions.parseAssignments(jvmargList, -1); // only -D entries
endpoint = resolvePlaceholders(endpoint, jvmargMap);
broker = resolvePlaceholders(broker, jvmargMap);
}
int i = broker.indexOf('?');
if (i > 0) {
broker = broker.substring(0, i);
}
String ep = "UIMA-AS:" + endpoint + ":" + broker;
return ep;
} else {
throw new IllegalArgumentException("Invalid DD:" + process_DD + ". Missing required element <inputQueue ...");
}
}
/**
* Check that dependencies are syntactically correct, and that a service doesn't depend on itself.
*
* Assumes that any placeholders have been resolved against the caller's environment
*
* @param endpoint This is the endpoint of the caller itself, for resolution ( to make sure it can resolve.). For
* jobs this must be null.
* @param dependency_string This is the whitespace-delimited string of service ids "I" am dependent upon.
*
* @return (possibly) corrected list of dependencies
*/
public static String check_service_dependencies(String endpoint, String dependency_string)
{
if ( dependency_string == null ) { // no dependencies to worry about
return null;
}
StringBuilder deps = new StringBuilder();
for (String d : dependency_string.split("\\s+")) {
String[] parts = d.split(":", 3);
String type = parts[0];
if (type.equals(ServiceType.UimaAs.decode())) {
// MUST have 2 ":" in it, and the broker must be a valid url
// UIMA-AS:queuename:broker
if (parts.length < 3) {
throw new IllegalArgumentException("Invalid UIMA-AS service id: " + d);
}
String qname = parts[1];
String broker = parts[2];
if (qname.equals("") || broker.equals("")) {
throw new IllegalArgumentException("Invalid syntax for UIMA-AS service id: " + d);
}
// this IS unused, it is here only to insure the string is parsed as a URL
@SuppressWarnings("unused")
URL url = null;
try {
url = new URL(null, broker, new TcpStreamHandler());
} catch (MalformedURLException e) {
throw new IllegalArgumentException("Invalid broker URL '" + broker + "' in service ID '" + d + "'");
}
// Finally strip the decorations as they are not part of a service name
// (Could check with url.getQuery() but cannot easily rebuild without it)
int ix = broker.indexOf('?');
if ( ix > 0) {
System.out.println("WARNING: Ignoring URL decorations on service ID " + d);
d = parts[0] + ":" + parts[1] + ":" + broker.substring(0, ix);
}
} else if (!type.equals(ServiceType.Custom.decode())) {
throw new IllegalArgumentException(
"Ill-formed or unsupported service type in dependency: '" + d + "'");
}
if (d.equals(endpoint)) {
throw new IllegalArgumentException("A service cannot depend on itself: " + d);
}
deps.append(d).append(" ");
}
return deps.substring(0, deps.length()-1);
}
/*
* Resolve any ${..} placeholders against a map of JVM arg values
*/
private static String resolvePlaceholders(String contents, Map<String,String> argMap) {
// Placeholders syntax ${<placeholder>}
Pattern pattern = Pattern.compile("\\$\\{(.*?)\\}"); // Stops on first '}'
Matcher matcher = pattern.matcher(contents);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
final String key = matcher.group(1);
String value = argMap.get(key);
if (value == null) {
throw new IllegalArgumentException("Undefined JVM property '" + key + "' in: " + contents);
}
// value may contain unreplaced ${..} sequence which looks like a group reference so replace as-is
matcher.appendReplacement(sb, "");
sb.append(value);
}
matcher.appendTail(sb);
return sb.toString();
}
}