/* Copyright 2014 MITRE Corporation * * 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 org.mitre.provenance.tools; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.UUID; import java.util.logging.Logger; /** * This is a garbage can class, with utility methods relevant to different places in the code. * @author moxious */ public class PLUSUtils { private static final String PLUS_OID_PREFIX = "urn:uuid:mitre:plus:"; /** Namespace used for XML-related junk */ public static final String NAMESPACE = "http://confluence.mitre.org/display/PLUS/"; private static Logger log = Logger.getLogger(PLUSUtils.class.getName()); /** * Generate a raw identifier used in PLUS object IDs * @return a String unique identifier (UUID, time-based random number) */ public static String generateRawUUID() { UUID u = UUID.randomUUID(); return u.toString(); } /** * Generate a new random UUID suitable for identifying data items and invocations. * @return a guaranteed unique string UUID */ public static String generateID() { return PLUS_OID_PREFIX + PLUSUtils.generateRawUUID(); } // End generateID /** * Determines whether a given identifier is a valid PLUS object ID (OID). Valid PLUS OIDs are generated * by this class, and always contain PLUS_OID_PREFIX * @param identifier the identifier to check * @return true if the identifier is a PLUS OID, false otherwise. * @see PLUSUtils#PLUS_OID_PREFIX * @see PLUSUtils#generateID() */ public static boolean isPLUSOID(String identifier) { return (identifier != null && identifier.startsWith(PLUS_OID_PREFIX)); } /** * Read the data available from a process, and print it to stdout. * @param p the process to read from. * @throws IOException */ public static void printOutput(Process p) throws IOException { InputStream is = p.getInputStream(); int av = is.available(); byte [] data = new byte [av]; for(int x=0; x<av; x++) { data[x] = (byte)is.read(); } String s = new String(data); PLUSUtils.log.fine("PROCESS OUTPUT:\n" + s); } /** * Takes a query string (in HTTP GET style) and returns a HashMap of its contents. Performs URLDecoding on * all values (but not keys). So for example, if passed x=1&y=2&z=3, it will return a HashMap with 3 keys * (x, y, and z) with corresponding values. * @param queryString an HTTP GET-style query string. * @return a HashMap of its contents */ public static HashMap<String,String> unpackQueryString(String queryString) { HashMap<String,String> results = new HashMap<String,String>(); String [] pieces = queryString.split("&"); for(int x=0; x<pieces.length; x++) { String [] vals = pieces[x].split("=", 2); if(vals.length != 2) continue; try { String key = vals[0]; String val = URLDecoder.decode(vals[1], "UTF-8"); results.put(key, val); } catch(UnsupportedEncodingException exc) { PLUSUtils.log.warning("PLUSUtils#unpackQueryString: Can't decode '" + pieces[x] + "': " + exc.getMessage()); } // End catch } // End for return results; } // End unpackQueryString /** * @deprecated */ public static String shortNameForURL(String url) { if(url == null || "".equals(url)) return "(None)"; try { URL u = new URL(url); String rsrcName = u.getPath(); String query = u.getQuery(); boolean hasParams = (query != null && !"".equals(query)); if(hasParams) return rsrcName + " (with parameters)"; else return rsrcName; } catch(Exception e) { ; } if(url.indexOf("&") > 0) return url.substring(0, url.indexOf("&")); return url; } // End shortNameForURL /** * Utility method for helping with JXTA messages. Take an input stream, and return its entire contents as a string. * Don't do this for very large input streams, because it's all in memory. * @deprecated */ public static String inputStreamToString(InputStream is) throws IOException { final byte[] buffer = new byte[0x10000]; StringBuilder out = new StringBuilder(); int read; do { read = is.read(buffer, 0, buffer.length); if (read>0) { out.append(new String(buffer, 0, read)); } } while (read>=0); return out.toString(); } // End inputStreamToString /** @deprecated */ public static String join(String delim, Object [] col) { if(col == null || col.length == 0) return ""; StringBuffer b = new StringBuffer(""); for(int x=0; x<col.length; x++) { b.append(""+col[x]); if(x < (col.length - 1)) b.append(delim); } return b.toString(); } /** * PLUS sometimes takes identifiers passed by other systems, which may be in any number * of encoding schemes, including URL, URI, and PLUS OID. This method normalizes the * identifier into something that is most likely to be found by the database. * @param id the ID to normalize * @return the best PLUS representation of the same ID. */ public static String normalizeExternalIdentifier(String id) { if(PLUSUtils.isPLUSOID(id)) return id; if(id.indexOf("?oid=") != -1) { String subid = id.substring(id.indexOf("?oid=")+5); if(PLUSUtils.isPLUSOID(subid)) return subid; } try { URI u = new URI(id); if(u.getScheme() != null && u.getHost() != null && "".equals(u.getPath())) return u + "/"; else return u.normalize().toString(); } catch(URISyntaxException exc) { ; } return id; } // End normalizeExternalIdentifier /** * Take a number of elapsed milliseconds, and describe it as a string. * @param elapsedMS a number of milliseconds elapsed. * @return a string of the form "X years, Y months, Z days, (etc)" */ public static String describeTimeSpan(long elapsedMS) { StringBuffer b = new StringBuffer(""); long days_ms = 1000 * 60 * 60 * 24; int days = 0; long years_ms = days_ms * 365; int years = 0; long hours_ms = 1000 * 60 * 60; int hours = 0; long minutes_ms = 1000 * 60; int minutes = 0; long seconds_ms = 1000; int seconds = 0; while(elapsedMS > years_ms) { years++; elapsedMS -= years_ms; } while(elapsedMS > days_ms) { days++; elapsedMS -= days_ms; } while(elapsedMS > hours_ms) { hours++; elapsedMS -= hours_ms; } while(elapsedMS > minutes_ms) { minutes++; elapsedMS -= minutes_ms; } while(elapsedMS > seconds_ms) { seconds++; elapsedMS -= seconds_ms; } if(years > 0) { b.append(years + " years "); } if(days > 0) { b.append(days + " days "); } if(hours > 0) { b.append(hours + " hours "); } if(minutes > 0) { b.append(minutes + " minutes "); } if(seconds > 0) { b.append(seconds + " seconds "); } if(elapsedMS > 0) { b.append(elapsedMS + "ms."); } return b.toString(); } // End describeTimeSpan /** * Normalizes a URI as specified in section 6.2.2 of RFC 3986 * @param uri a URI * @return an RFC 3986 URI normalized according to section 6.2.2. * @throws URISyntaxException * @throws UnsupportedEncodingException */ public static URI normalizeURI(String uri) throws URISyntaxException, UnsupportedEncodingException { return normalizeURI(new URI(uri)); } /** * Normalizes a URI as specified in section 6.2.2 of RFC 3986. * At present, this does nothing for opaque URIs (such as URNs, and mailto:foo@bar.com). For non-opaque * URIs, it standardizes the case of escaped octets, hostname, fixes port references, alphebetizes and * properly encodes query string parameters, and resolves relative paths. * @param uri a URI * @return an RFC 3986 URI normalized according to section 6.2.2. * @throws URISyntaxException * @throws UnsupportedEncodingException */ public static URI normalizeURI(URI uri) throws URISyntaxException, UnsupportedEncodingException { if(uri.isOpaque()) return uri; uri = uri.normalize(); String scheme = uri.getScheme(); String userInfo = uri.getUserInfo(); String host = uri.getHost(); String path = uri.getPath(); String query = uri.getQuery(); String fragment = uri.getFragment(); Integer port = uri.getPort(); if(path == null || "".equals(path)) path = "/"; if(scheme != null) scheme = scheme.toLowerCase(); if(host != null) host = host.toLowerCase(); if(port != null && port.equals(getPortForScheme(scheme))) port = null; if(port != null) return new URI(scheme, userInfo, host, port, URLEncoder.encode(path, "UTF-8").replaceAll("%2F", "/"), normalizeQueryString(query), (fragment == null ? null : URLEncoder.encode(fragment, "UTF-8"))); else { String authority = host; if(userInfo != null) authority = userInfo + "@" + host; return new URI(scheme, authority, URLEncoder.encode(path, "UTF-8").replaceAll("%2F", "/"), normalizeQueryString(query), (fragment == null ? null : URLEncoder.encode(fragment, "UTF-8"))); } // End else } // End normalizeURI /** * Given an un-encoded URI query string, this will return a normalized, properly encoded URI query string. * <b>Important:</b> This method uses java's URLEncoder, which returns things that are * application/x-www-form-urlencoded, instead of things that are properly octet-esacped as the URI spec * requires. As a result, some substitutions are made to properly translate space characters to meet the * URI spec. * @param queryString * @return */ private static String normalizeQueryString(String queryString) throws UnsupportedEncodingException { if("".equals(queryString) || queryString == null) return queryString; String [] pieces = queryString.split("&"); HashMap<String,String>kvp = new HashMap<String,String>(); StringBuffer builder = new StringBuffer(""); for(int x=0; x<pieces.length; x++) { String [] bs = pieces[x].split("=", 2); bs[0] = URLEncoder.encode(bs[0], "UTF-8"); if(bs.length == 1) kvp.put(bs[0], null); else { kvp.put(bs[0], URLEncoder.encode(bs[1], "UTF-8").replaceAll("\\+", "%20")); } } // Sort the keys alphabetically, ignoring case. ArrayList<String>keys = new ArrayList<String>(kvp.keySet()); Collections.sort(keys, new Comparator<String>() { public int compare(String o1, String o2) { return o1.compareToIgnoreCase(o2); } }); // With the alphabetic list of parameter names, re-build the query string. for(int x=0; x<keys.size(); x++) { // Some parameters have no value, and are simply present. If so, we put null in kvp, // and we just put the parameter name, no "=value". if(kvp.get(keys.get(x)) == null) builder.append(keys.get(x)); else builder.append(keys.get(x) + "=" + kvp.get(keys.get(x))); if(x < (keys.size() -1)) builder.append("&"); } return builder.toString(); } // End normalizeQueryString /** * See http://www.iana.org/assignments/port-numbers. This is a partial list of only the most common. * @param scheme a scheme within a URI (such as http, ftp, ssh, etc) * @return the standard port number for that scheme. */ private static Integer getPortForScheme(String scheme) { scheme = scheme.toLowerCase(); if("http".equals(scheme)) return 80; if("ftp".equals(scheme)) return 21; if("ssh".equals(scheme)) return 22; if("telnet".equals(scheme)) return 23; if("gopher".equals(scheme)) return 70; if("http-alt".equals(scheme)) return 8080; if("radan-http".equals(scheme)) return 8088; if("dnsix".equals(scheme)) return 90; if("echo".equals(scheme)) return 7; if("daytime".equals(scheme)) return 13; if("smtp".equals(scheme)) return 25; if("time".equals(scheme)) return 37; return null; } } // End PLUSUtils