diff options
Diffstat (limited to 'winery/org.eclipse.winery.common/src/main/java/org/eclipse/winery/common/Util.java')
-rw-r--r-- | winery/org.eclipse.winery.common/src/main/java/org/eclipse/winery/common/Util.java | 603 |
1 files changed, 603 insertions, 0 deletions
diff --git a/winery/org.eclipse.winery.common/src/main/java/org/eclipse/winery/common/Util.java b/winery/org.eclipse.winery.common/src/main/java/org/eclipse/winery/common/Util.java new file mode 100644 index 0000000..702bf41 --- /dev/null +++ b/winery/org.eclipse.winery.common/src/main/java/org/eclipse/winery/common/Util.java @@ -0,0 +1,603 @@ +/******************************************************************************* + * Copyright (c) 2013 University of Stuttgart. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * and the Apache License 2.0 which both accompany this distribution, + * and are available at http://www.eclipse.org/legal/epl-v10.html + * and http://www.apache.org/licenses/LICENSE-2.0 + * + * Contributors: + * Oliver Kopp - initial API and implementation + *******************************************************************************/ +package org.eclipse.winery.common; + +import java.io.ByteArrayOutputStream; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.List; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchema; +import javax.xml.namespace.QName; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.apache.commons.lang3.StringUtils; +import org.apache.taglibs.standard.functions.Functions; +import org.eclipse.winery.common.ids.GenericId; +import org.eclipse.winery.common.ids.definitions.ArtifactTemplateId; +import org.eclipse.winery.common.ids.definitions.EntityTemplateId; +import org.eclipse.winery.common.ids.definitions.EntityTypeId; +import org.eclipse.winery.common.ids.definitions.EntityTypeImplementationId; +import org.eclipse.winery.common.ids.definitions.PolicyTemplateId; +import org.eclipse.winery.common.ids.definitions.ServiceTemplateId; +import org.eclipse.winery.common.ids.definitions.TOSCAComponentId; +import org.eclipse.winery.common.ids.definitions.imports.GenericImportId; +import org.eclipse.winery.common.ids.definitions.imports.XSDImportId; +import org.eclipse.winery.common.ids.elements.TOSCAElementId; +import org.eclipse.winery.model.tosca.TEntityType; +import org.eclipse.winery.model.tosca.TExtensibleElements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +public class Util { + + private static final Logger logger = LoggerFactory.getLogger(Util.class); + + public static final String FORBIDDEN_CHARACTER_REPLACEMENT = "_"; + + + public static String URLdecode(String s) { + try { + return URLDecoder.decode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(); + } + } + + public static String URLencode(String s) { + try { + return URLEncoder.encode(s, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(); + } + } + + public static String DoubleURLencode(String s) { + return Util.URLencode(Util.URLencode(s)); + } + + /** + * Encodes the namespace and the localname of the given qname, separated by + * "/" + * + * @return <double encoded namespace>"/"<double encoded localname> + */ + public static String DoubleURLencode(QName qname) { + String ns = Util.DoubleURLencode(qname.getNamespaceURI()); + String localName = Util.DoubleURLencode(qname.getLocalPart()); + return ns + "/" + localName; + } + + public static boolean isRelativeURI(String uri) { + URI u; + try { + u = new URI(uri); + } catch (URISyntaxException e) { + Util.logger.debug(e.getMessage(), e); + // fallback + return false; + } + return !u.isAbsolute(); + } + + /** + * @param c the element directly nested below a definitions element in XML + */ + public static String getURLpathFragmentForCollection(Class<? extends TExtensibleElements> c) { + String res = c.getName().toLowerCase(); + int lastDot = res.lastIndexOf('.'); + // classname is something like <package>.T<type>. We are only interested + // in "<type>". Therefore "+2" from the dot onwards + res = res.substring(lastDot + 2); + res = res + "s"; + return res; + } + + public static String getEverythingBetweenTheLastDotAndBeforeId(Class<? extends GenericId> cls) { + String res = cls.getName(); + // Everything between the last "." and before "Id" is the Type + int dotIndex = res.lastIndexOf('.'); + assert (dotIndex >= 0); + return res.substring(dotIndex + 1, res.length() - "Id".length()); + } + + public static String getTypeForElementId(Class<? extends TOSCAElementId> idClass) { + return Util.getEverythingBetweenTheLastDotAndBeforeId(idClass); + } + + /** + * @return Singular type name for the given id. E.g., "ServiceTemplateId" + * gets "ServiceTemplate" + */ + public static String getTypeForComponentId(Class<? extends TOSCAComponentId> idClass) { + return Util.getEverythingBetweenTheLastDotAndBeforeId(idClass); + } + + /** + * Returns the root path fragment for the given + * AbstractComponentIntanceResource + * + * With trailing slash + * + * @return [ComponentName]s/ + */ + public static String getRootPathFragment(Class<? extends TOSCAComponentId> idClass) { + // quick handling of imports special case + // in the package naming, all other component instances have a this intermediate location, but not in the URLs + // The package handling is in {@link org.eclipse.winery.repository.Utils.getIntermediateLocationStringForType(String, String)} + String res; + if (GenericImportId.class.isAssignableFrom(idClass)) { + // this fires if idClass is a sub class from ImportCollectionId + // special treatment for imports + res = "imports/"; + if (XSDImportId.class.isAssignableFrom(idClass)) { + res = res + "http%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema/"; + } else { + throw new IllegalStateException("Not possible to determine local storage for generic imports class"); + } + // we have the complete root path fragment + return res; + } else { + res = ""; + } + res = res + Util.getTypeForComponentId(idClass); + res = res.toLowerCase(); + res = res + "s"; + res = res + "/"; + return res; + } + + /** + * Just calls @link{qname2href} + * + * Introduced because of JSP error + * "The method qname2href(String, Class<? extends TExtensibleElements>, QName) in the type Util is not applicable for the arguments (String, Class<TNodeType>, QName, String)" + */ + public static String qname2hrefWithName(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname, String name) { + return Util.qname2href(repositoryUrl, element, qname, name); + } + + /** + * + * @param repositoryUrl the URL to the repository + * @param element the element directly nested below a definitions element in + * XML + * @param qname the QName of the element + * @param name (optional) if not null, the name to display as text in the + * reference. Default: localName of the QName + * @return an <code>a</code> HTML element pointing to the given id + */ + public static String qname2href(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname, String name) { + if (StringUtils.isEmpty(repositoryUrl)) { + throw new IllegalArgumentException("Repository URL must not be empty."); + } + if (element == null) { + throw new IllegalArgumentException("Element class must not be null."); + } + if (qname == null) { + return "(none)"; + } + + String absoluteURL = repositoryUrl + "/" + Util.getURLpathFragmentForCollection(element) + "/" + Util.DoubleURLencode(qname.getNamespaceURI()) + "/" + Util.DoubleURLencode(qname.getLocalPart()); + + if (name == null) { + // fallback if no name is given + name = qname.getLocalPart(); + } + // sanitize name + name = Functions.escapeXml(name); + + String res = "<a target=\"_blank\" data-qname=\"" + qname + "\" href=\"" + absoluteURL + "\">" + name + "</a>"; + return res; + } + + /** + * + * @param repositoryUrl the URL to the repository + * @param element the element directly nested below a definitions element in + * XML + * @param qname the QName of the element + * @return an <code>a</code> HTML element pointing to the given id + */ + public static String qname2href(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname) { + return Util.qname2href(repositoryUrl, element, qname, null); + } + + /** + * Returns a visual rendering of minInstances + * + * @param minInstances the value to render + */ + public static String renderMinInstances(Integer minInstances) { + if ((minInstances == null) || (minInstances == 1)) { + // == null: default value: display nothing -- *never* happens: + // the function *always* returns 1 even, if no explicit value is set. Therefore, we also display "" if the default value 1 is set + return ""; + } else { + return Integer.toString(minInstances); + } + } + + /** + * Returns a visual rendering of maxInstances + * + * @param maxInstances the value to render + */ + public static String renderMaxInstances(String maxInstances) { + if ((maxInstances == null) || (maxInstances.equals("1"))) { + // default value display nothing + // "1" is returned even if no explicit value has been set. + return ""; + } else if (maxInstances.equals("unbounded")) { + return "∞"; + } else { + // maxInstance is a plain integer + // return as is + return maxInstances; + } + } + + /** + * @return the local name of a Class representing a TOSCA element + */ + private static String getLocalName(@SuppressWarnings("rawtypes") Class clazz) { + String localName = clazz.getName(); + // a class defined within another class is written as superclass$class. E.g., EntityTemplate$Properties + // We use the real class name + int pos = localName.lastIndexOf('$'); + if (pos == -1) { + pos = localName.lastIndexOf('.'); + } + localName = localName.substring(pos + 1); + if (localName.equals("TDocumentation")) { + // special case for documentation: the local name starts with a lower case letter + localName = "documentation"; + } else if (localName.startsWith("T")) { + localName = localName.substring(1); + } + return localName; + } + + public static <T extends Object> JAXBElement<T> getJAXBElement(Class<T> clazz, T obj) { + String namespace = null; + XmlRootElement xmlRootElement = clazz.getAnnotation(XmlRootElement.class); + if (xmlRootElement != null) { + namespace = xmlRootElement.namespace(); + if ("##default".equals(namespace)) { + XmlSchema xmlSchema = clazz.getPackage().getAnnotation(XmlSchema.class); + if (xmlSchema != null) { + namespace = xmlSchema.namespace(); + } else { + // trigger default handling + namespace = null; + } + } + } + if (namespace == null) { + // fallback non-specified namespaces + namespace = org.eclipse.winery.common.constants.Namespaces.TOSCA_NAMESPACE; + } + String localName = Util.getLocalName(clazz); + QName qname = new QName(namespace, localName); + JAXBElement<T> rootElement = new JAXBElement<T>(qname, clazz, obj); + return rootElement; + } + + /** + * Method similar to {@link + * org.eclipse.winery.repository.Utils.getXMLAsString(Class, Object)}. + * + * Differences: + * <ul> + * <li>XML processing instruction is not included in the header</li> + * <li>JAXBcontext is created at each call</li> + * </ul> + */ + public static <T extends Object> String getXMLAsString(Class<T> clazz, T obj) throws Exception { + // copied from Utils java, but we create an own JAXBcontext here + // JAXBSupport cannot be used as this relies on a MockElement, which we do not want to factor out to winery.common + + JAXBContext context; + try { + // For winery classes, eventually the package+jaxb.index method could be better. See http://stackoverflow.com/a/3628525/873282 + // @formatter:off + context = JAXBContext.newInstance( + TEntityType.class); + // @formatter:on + } catch (JAXBException e) { + throw new IllegalStateException(e); + } + + JAXBElement<T> rootElement = Util.getJAXBElement(clazz, obj); + Marshaller m = context.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + m.setProperty(Marshaller.JAXB_FRAGMENT, true); + // m.setProperty("com.sun.xml.bind.namespacePrefixMapper", JAXBSupport.prefixMapper); + + StringWriter w = new StringWriter(); + try { + m.marshal(rootElement, w); + } catch (JAXBException e) { + throw new IllegalStateException(e); + } + String res = w.toString(); + return res; + } + + public static String getXMLAsString(Element el) { + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer t; + try { + t = tf.newTransformer(); + } catch (TransformerConfigurationException e) { + throw new IllegalStateException("Could not instantiate Transformer", e); + } + t.setOutputProperty(OutputKeys.INDENT, "yes"); + Source source = new DOMSource(el); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + Result target = new StreamResult(os); + try { + t.transform(source, target); + } catch (TransformerException e) { + Util.logger.debug(e.getMessage(), e); + throw new IllegalStateException("Could not transform dom node to string", e); + } + return os.toString(); + } + + /** + * Determines whether the instance belonging to the given id supports the + * "name" attribute. This cannot be done using the super class as the TOSCA + * specification treats that differently in the case of EntityTemplates + * + * NOTE: The respective subclasses of AbstractComponentInstanceResource have + * to implement {@link org.eclipse.winery.repository.resources.IHasName} + * + * @param id the id to test + * @return true if the TOSCA model class belonging to the given id supports + * the method "getName()" in addition to "getId()" + */ + public static boolean instanceSupportsNameAttribute(Class<? extends TOSCAComponentId> idClass) { + if (ServiceTemplateId.class.isAssignableFrom(idClass)) { + return true; + } else if ((EntityTypeId.class.isAssignableFrom(idClass)) || (EntityTypeImplementationId.class.isAssignableFrom(idClass))) { + // name is available, but no id attribute + return false; + } else if (GenericImportId.class.isAssignableFrom(idClass)) { + return false; + } else { + assert (EntityTemplateId.class.isAssignableFrom(idClass)); + if (ArtifactTemplateId.class.isAssignableFrom(idClass)) { + return true; + } else if (PolicyTemplateId.class.isAssignableFrom(idClass)) { + return true; + } else { + throw new IllegalStateException("Unimplemented branch to determine if getName() exists"); + } + } + } + + public static String getLastURIPart(String loc) { + int posSlash = loc.lastIndexOf('/'); + String fileName = loc.substring(posSlash + 1); + return fileName; + } + + /** + * Determines a color belonging to the given name + */ + public static String getColor(String name) { + int hash = name.hashCode(); + // trim to 3*8=24 bits + hash = hash & 0xFFFFFF; + // check if color is more than #F0F0F0, i.e., too light + if (((hash & 0xF00000) >= 0xF00000) && (((hash & 0x00F000) >= 0x00F000) && ((hash & 0x0000F0) >= 0x0000F0))) { + // set one high bit to zero for each channel. That makes the overall color darker + hash = hash & 0xEFEFEF; + } + String colorStr = String.format("#%06x", hash); + return colorStr; + } + + /** + * Determines the name of the CSS class used for relationshipTypes at + * nodeTemplateRenderer.tag + */ + public static String makeCSSName(String namespace, String localName) { + // according to http://stackoverflow.com/a/79022/873282 everything is allowed + // However, {namespace}id does NOT work + String res = namespace + "_" + localName; + res = res.replaceAll("[^\\w\\d_]", "_"); + return res; + } + + /** + * @see {@link org.eclipse.winery.common.Util.makeCSSName(String, String)} + */ + public static String makeCSSName(QName qname) { + return Util.makeCSSName(qname.getNamespaceURI(), qname.getLocalPart()); + } + + public static SortedMap<String, SortedSet<String>> convertQNameListToNamespaceToLocalNameList(List<QName> list) { + SortedMap<String, SortedSet<String>> res = new TreeMap<>(); + for (QName qname : list) { + SortedSet<String> localNameSet = res.get(qname.getNamespaceURI()); + if (localNameSet == null) { + localNameSet = new TreeSet<>(); + res.put(qname.getNamespaceURI(), localNameSet); + } + localNameSet.add(qname.getLocalPart()); + } + return res; + } + + public static String namespaceToJavaPackage(String namespace) { + URI uri; + try { + uri = new URI(namespace); + } catch (URISyntaxException e) { + Util.logger.debug(e.getMessage(), e); + return "uri.invalid"; + } + StringBuilder sb = new StringBuilder(); + + String host = uri.getHost(); + if (host != null) { + Util.addReversed(sb, host, "\\."); + } + + String path = uri.getPath(); + if (!path.equals("")) { + if (path.startsWith("/")) { + // remove first slash + path = path.substring(1); + } + + // and then handle the string + Util.addAsIs(sb, path, "/"); + } + + // remove the final dot + sb.replace(sb.length() - 1, sb.length(), ""); + + return Util.cleanName(sb.toString()); + } + + private static String cleanName(String s) { + // TODO: Integrate with other name cleaning functions. "." should not be replaced as it is used as separator in the java package name + // @formatter:off + return s.replace(":", Util.FORBIDDEN_CHARACTER_REPLACEMENT) + .replace("/", Util.FORBIDDEN_CHARACTER_REPLACEMENT) + .replace(" ", Util.FORBIDDEN_CHARACTER_REPLACEMENT) + .replace("-", Util.FORBIDDEN_CHARACTER_REPLACEMENT); + // @formatter:on + } + + + /* + * Valid chars: See + * <ul> + * <li>http://www.w3.org/TR/REC-xml-names/#NT-NCName</li> + * <li>http://www.w3.org/TR/REC-xml/#NT-Name</li> + * </ul> + */ + // NameCharRange \u10000-\ueffff is not supported by Java + private static final String NCNameStartChar_RegExp = "[A-Z_a-z\u00c0-\u00d6\u00d8\u00f6\u00f8\u02ff\u0370\u037d\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]"; + private static final String NCNameChar_RegExp = Util.NCNameStartChar_RegExp + "|[-\\.0-9\u00B7\u0300-\u036F\u203F-\u2040]"; + private static final Pattern NCNameStartChar_Pattern = Pattern.compile(Util.NCNameStartChar_RegExp); + private static final Pattern NCNameChar_RegExp_Pattern = Pattern.compile(Util.NCNameChar_RegExp); + + + /** + * Removes all non-NCName characters from the given string and returns the + * result + * + * This function should be consistent with + * org.eclipse.winery.common.Util.cleanName(String) + * + * TODO: This method seems to be equal to {@link + * org.eclipse.winery.repository.Utils.createXMLidAsString(String)}. These + * methods should be merged. + * + */ + public static String makeNCName(String text) { + if (StringUtils.isEmpty(text)) { + return text; + } + + StringBuffer res = new StringBuffer(); + + // handle start + String start = text.substring(0, 1); + Matcher m = Util.NCNameStartChar_Pattern.matcher(start); + if (m.matches()) { + res.append(start); + } else { + // not a valid character + res.append("_"); + } + + // handle remaining characters; + for (int i = 1; i < text.length(); i++) { + String s = text.substring(i, i + 1); + m = Util.NCNameChar_RegExp_Pattern.matcher(s); + if (m.matches()) { + res.append(s); + } else { + // not a valid character + res.append("_"); + } + } + + return res.toString(); + } + + private static void addAsIs(StringBuilder sb, String s, String separator) { + if (s.isEmpty()) { + return; + } + String[] split = s.split(separator); + for (int i = 0; i < split.length; i++) { + sb.append(split[i]); + sb.append("."); + } + } + + private static void addReversed(StringBuilder sb, String s, String separator) { + String[] split = s.split(separator); + for (int i = split.length - 1; i >= 0; i--) { + sb.append(split[i]); + sb.append("."); + } + } + + /** + * Bridge to client.getType(). Just calls client getType(), used by + * functions.tld. + * + * We suppress compiler warnings as JSP 2.0 do not offer support for + * generics, but we're using JSP 2.0... + * + * @param client the repository client to use + * @param qname the QName to resolve + * @param clazz the class the QName is describing + * @return {@inheritDoc} + */ + @SuppressWarnings({"rawtypes", "unchecked"}) + public static org.eclipse.winery.model.tosca.TEntityType getType(org.eclipse.winery.common.interfaces.IWineryRepository client, javax.xml.namespace.QName qname, java.lang.Class clazz) { + return client.getType(qname, clazz); + } +} |