// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.quickstart; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import org.eclipse.jetty.util.resource.Resource; /** * Normalize Attribute to String. * <p> * Replaces and expands: * <ul> * <li>${WAR}</li> * <li>${WAR.path}</li> * <li>${WAR.uri}</li> * <li>${jetty.base}</li> * <li>${jetty.base.uri}</li> * <li>${jetty.home}</li> * <li>${jetty.home.uri}</li> * <li>${user.home}</li> * <li>${user.home.uri}</li> * <li>${user.dir}</li> * <li>${user.dir.uri}</li> * </ul> */ public class AttributeNormalizer { private static final Logger LOG = Log.getLogger(AttributeNormalizer.class); private static final Pattern __propertyPattern = Pattern.compile("(?<=[^$]|^)\\$\\{([^}]*)\\}"); private static class Attribute { final String key; final String value; final int weight; public Attribute(String key, String value, int weight) { this.key = key; this.value = value; this.weight = weight; } } private static URI toCanonicalURI(URI uri) { uri = uri.normalize(); String ascii = uri.toASCIIString(); if (ascii.endsWith("/")) { try { uri = new URI(ascii.substring(0,ascii.length()-1)); } catch(URISyntaxException e) { throw new IllegalArgumentException(e); } } return uri; } private static Path toCanonicalPath(String path) { if (path == null) return null; if (path.length()>1 && path.endsWith("/")) path = path.substring(0,path.length()-1); return toCanonicalPath(FileSystems.getDefault().getPath(path)); } private static Path toCanonicalPath(Path path) { if (path == null) { return null; } if (Files.exists(path)) { try { return path.toRealPath(); } catch (IOException e) { throw new IllegalArgumentException(e); } } return path.toAbsolutePath(); } private static class PathAttribute extends Attribute { public final Path path; public PathAttribute(String key, Path path, int weight) { super(key,path.toString(),weight); this.path = path; } @Override public String toString() { return String.format("PathAttribute[%s=>%s]",key,path); } } private static class URIAttribute extends Attribute { public final URI uri; public URIAttribute(String key, URI uri, int weight) { super(key,uri.toASCIIString(),weight); this.uri = uri; } @Override public String toString() { return String.format("URIAttribute[%s=>%s]",key,uri); } } private static Comparator<Attribute> attrComparator = new Comparator<Attribute>() { @Override public int compare(Attribute o1, Attribute o2) { if( (o1.value == null) && (o2.value != null) ) { return -1; } if( (o1.value != null) && (o2.value == null) ) { return 1; } if( (o1.value == null) && (o2.value == null) ) { return 0; } // Different lengths? int diff = o2.value.length() - o1.value.length(); if(diff != 0) { return diff; } // Different names? diff = o2.value.compareTo(o1.value); if(diff != 0) { return diff; } // The paths are the same, base now on weight return o2.weight - o1.weight; } }; private static void add(List<PathAttribute>paths,List<URIAttribute> uris,String key,int weight) { String value = System.getProperty(key); if (value!=null) { Path path = toCanonicalPath(value); paths.add(new PathAttribute(key,path,weight)); uris.add(new URIAttribute(key+".uri",toCanonicalURI(path.toUri()),weight)); } } private URI warURI; private Map<String,Attribute> attributes = new HashMap<>(); private List<PathAttribute> paths = new ArrayList<>(); private List<URIAttribute> uris = new ArrayList<>(); public AttributeNormalizer(Resource baseResource) { if (baseResource==null) throw new IllegalArgumentException("No base resource!"); warURI = toCanonicalURI(baseResource.getURI()); if (!warURI.isAbsolute()) throw new IllegalArgumentException("WAR URI is not absolute: " + warURI); add(paths,uris,"jetty.base",9); add(paths,uris,"jetty.home",8); add(paths,uris,"user.home",7); add(paths,uris,"user.dir",6); if (warURI.getScheme().equalsIgnoreCase("file")) paths.add(new PathAttribute("WAR.path",toCanonicalPath(new File(warURI).toString()),10)); uris.add(new URIAttribute("WAR.uri", warURI,9)); // preferred encoding uris.add(new URIAttribute("WAR", warURI,8)); // legacy encoding Collections.sort(paths,attrComparator); Collections.sort(uris,attrComparator); Stream.concat(paths.stream(),uris.stream()).forEach(a->attributes.put(a.key,a)); if (LOG.isDebugEnabled()) { for (Attribute attr : attributes.values()) { LOG.debug(attr.toString()); } } } /** * Normalize a URI, URL, or File reference by replacing known attributes with ${key} attributes. * * @param o the object to normalize into a string * @return the string representation of the object, with expansion keys. */ public String normalize(Object o) { try { // Find a URI URI uri = null; Path path = null; if (o instanceof URI) uri = toCanonicalURI(((URI)o)); else if (o instanceof Resource) uri = toCanonicalURI(((Resource)o).getURI()); else if (o instanceof URL) uri = toCanonicalURI(((URL)o).toURI()); else if (o instanceof File) path = ((File)o).getAbsoluteFile().getCanonicalFile().toPath(); else if (o instanceof Path) path = (Path)o; else { String s = o.toString(); try { uri = new URI(s); if (uri.getScheme() == null) { // Unknown scheme? not relevant to normalize return s; } } catch(URISyntaxException e) { // This path occurs for many reasons, but most common is when this // is executed on MS Windows, on a string like "D:\jetty" // and the new URI() fails for // java.net.URISyntaxException: Illegal character in opaque part at index 2: D:\jetty return s; } } if (uri!=null) { if ("jar".equalsIgnoreCase(uri.getScheme())) { String raw = uri.getRawSchemeSpecificPart(); int bang = raw.indexOf("!/"); String normal = normalize(raw.substring(0,bang)); String suffix = raw.substring(bang); return "jar:" + normal + suffix; } else { if(uri.isAbsolute()) { return normalizeUri(uri); } } } else if (path!=null) return normalizePath(path); } catch (Exception e) { LOG.warn(e); } return String.valueOf(o); } protected String normalizeUri(URI uri) { for (URIAttribute a : uris) { try { if (uri.compareTo(a.uri)==0) return String.format("${%s}",a.key); if (!a.uri.getScheme().equalsIgnoreCase(uri.getScheme())) continue; if (a.uri.getHost()==null && uri.getHost()!=null) continue; if (a.uri.getHost()!=null && !a.uri.getHost().equals(uri.getHost())) continue; if (a.uri.getPath().equals(uri.getPath())) return a.value; if (!uri.getPath().startsWith(a.uri.getPath())) continue; String s = uri.getPath().substring(a.uri.getPath().length()); if (s.charAt(0)!='/') continue; return String.format("${%s}%s",a.key,new URI(s).toASCIIString()); } catch(URISyntaxException e) { LOG.ignore(e); } } return uri.toASCIIString(); } protected String normalizePath(Path path) { for (PathAttribute a : paths) { try { if (path.equals(a.path) || Files.isSameFile(path,a.path)) return String.format("${%s}",a.key); } catch (IOException ignore) { LOG.ignore(ignore); } if (path.startsWith(a.path)) return String.format("${%s}%c%s",a.key,File.separatorChar,a.path.relativize(path).toString()); } return path.toString(); } public String expand(String str) { return expand(str,new Stack<String>()); } public String expand(String str, Stack<String> seenStack) { if (str == null) { return str; } if (str.indexOf("${") < 0) { // Contains no potential expressions. return str; } Matcher mat = __propertyPattern.matcher(str); StringBuilder expanded = new StringBuilder(); int offset = 0; String property; String value; while (mat.find(offset)) { property = mat.group(1); // Loop detection if (seenStack.contains(property)) { StringBuilder err = new StringBuilder(); err.append("Property expansion loop detected: "); int idx = seenStack.lastIndexOf(property); for (int i = idx; i < seenStack.size(); i++) { err.append(seenStack.get(i)); err.append(" -> "); } err.append(property); throw new RuntimeException(err.toString()); } seenStack.push(property); // find property name expanded.append(str.subSequence(offset,mat.start())); // get property value value = getString(property); if (value == null) { if(LOG.isDebugEnabled()) LOG.debug("Unable to expand: {}",property); expanded.append(mat.group()); } else { // recursively expand value = expand(value,seenStack); expanded.append(value); } // update offset offset = mat.end(); } // leftover expanded.append(str.substring(offset)); // special case for "$$" if (expanded.indexOf("$$") >= 0) { return expanded.toString().replaceAll("\\$\\$","\\$"); } return expanded.toString(); } private String getString(String property) { if(property==null) { return null; } Attribute a = attributes.get(property); if (a!=null) return a.value; // Use system properties next return System.getProperty(property); } }