702bf41d52831eb4d186070ebb6d3f5c990da0b5
[vfc/nfvo/wfengine.git] / winery / org.eclipse.winery.common / src / main / java / org / eclipse / winery / common / Util.java
1 /*******************************************************************************
2  * Copyright (c) 2013 University of Stuttgart.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * and the Apache License 2.0 which both accompany this distribution,
6  * and are available at http://www.eclipse.org/legal/epl-v10.html
7  * and http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Contributors:
10  *     Oliver Kopp - initial API and implementation
11  *******************************************************************************/
12 package org.eclipse.winery.common;
13
14 import java.io.ByteArrayOutputStream;
15 import java.io.StringWriter;
16 import java.io.UnsupportedEncodingException;
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.net.URLDecoder;
20 import java.net.URLEncoder;
21 import java.util.List;
22 import java.util.SortedMap;
23 import java.util.SortedSet;
24 import java.util.TreeMap;
25 import java.util.TreeSet;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import javax.xml.bind.JAXBContext;
30 import javax.xml.bind.JAXBElement;
31 import javax.xml.bind.JAXBException;
32 import javax.xml.bind.Marshaller;
33 import javax.xml.bind.annotation.XmlRootElement;
34 import javax.xml.bind.annotation.XmlSchema;
35 import javax.xml.namespace.QName;
36 import javax.xml.transform.OutputKeys;
37 import javax.xml.transform.Result;
38 import javax.xml.transform.Source;
39 import javax.xml.transform.Transformer;
40 import javax.xml.transform.TransformerConfigurationException;
41 import javax.xml.transform.TransformerException;
42 import javax.xml.transform.TransformerFactory;
43 import javax.xml.transform.dom.DOMSource;
44 import javax.xml.transform.stream.StreamResult;
45
46 import org.apache.commons.lang3.StringUtils;
47 import org.apache.taglibs.standard.functions.Functions;
48 import org.eclipse.winery.common.ids.GenericId;
49 import org.eclipse.winery.common.ids.definitions.ArtifactTemplateId;
50 import org.eclipse.winery.common.ids.definitions.EntityTemplateId;
51 import org.eclipse.winery.common.ids.definitions.EntityTypeId;
52 import org.eclipse.winery.common.ids.definitions.EntityTypeImplementationId;
53 import org.eclipse.winery.common.ids.definitions.PolicyTemplateId;
54 import org.eclipse.winery.common.ids.definitions.ServiceTemplateId;
55 import org.eclipse.winery.common.ids.definitions.TOSCAComponentId;
56 import org.eclipse.winery.common.ids.definitions.imports.GenericImportId;
57 import org.eclipse.winery.common.ids.definitions.imports.XSDImportId;
58 import org.eclipse.winery.common.ids.elements.TOSCAElementId;
59 import org.eclipse.winery.model.tosca.TEntityType;
60 import org.eclipse.winery.model.tosca.TExtensibleElements;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63 import org.w3c.dom.Element;
64
65 public class Util {
66         
67         private static final Logger logger = LoggerFactory.getLogger(Util.class);
68         
69         public static final String FORBIDDEN_CHARACTER_REPLACEMENT = "_";
70         
71         
72         public static String URLdecode(String s) {
73                 try {
74                         return URLDecoder.decode(s, "UTF-8");
75                 } catch (UnsupportedEncodingException e) {
76                         throw new IllegalStateException();
77                 }
78         }
79         
80         public static String URLencode(String s) {
81                 try {
82                         return URLEncoder.encode(s, "UTF-8");
83                 } catch (UnsupportedEncodingException e) {
84                         throw new IllegalStateException();
85                 }
86         }
87         
88         public static String DoubleURLencode(String s) {
89                 return Util.URLencode(Util.URLencode(s));
90         }
91         
92         /**
93          * Encodes the namespace and the localname of the given qname, separated by
94          * "/"
95          * 
96          * @return <double encoded namespace>"/"<double encoded localname>
97          */
98         public static String DoubleURLencode(QName qname) {
99                 String ns = Util.DoubleURLencode(qname.getNamespaceURI());
100                 String localName = Util.DoubleURLencode(qname.getLocalPart());
101                 return ns + "/" + localName;
102         }
103         
104         public static boolean isRelativeURI(String uri) {
105                 URI u;
106                 try {
107                         u = new URI(uri);
108                 } catch (URISyntaxException e) {
109                         Util.logger.debug(e.getMessage(), e);
110                         // fallback
111                         return false;
112                 }
113                 return !u.isAbsolute();
114         }
115         
116         /**
117          * @param c the element directly nested below a definitions element in XML
118          */
119         public static String getURLpathFragmentForCollection(Class<? extends TExtensibleElements> c) {
120                 String res = c.getName().toLowerCase();
121                 int lastDot = res.lastIndexOf('.');
122                 // classname is something like <package>.T<type>. We are only interested
123                 // in "<type>". Therefore "+2" from the dot onwards
124                 res = res.substring(lastDot + 2);
125                 res = res + "s";
126                 return res;
127         }
128         
129         public static String getEverythingBetweenTheLastDotAndBeforeId(Class<? extends GenericId> cls) {
130                 String res = cls.getName();
131                 // Everything between the last "." and before "Id" is the Type
132                 int dotIndex = res.lastIndexOf('.');
133                 assert (dotIndex >= 0);
134                 return res.substring(dotIndex + 1, res.length() - "Id".length());
135         }
136         
137         public static String getTypeForElementId(Class<? extends TOSCAElementId> idClass) {
138                 return Util.getEverythingBetweenTheLastDotAndBeforeId(idClass);
139         }
140         
141         /**
142          * @return Singular type name for the given id. E.g., "ServiceTemplateId"
143          *         gets "ServiceTemplate"
144          */
145         public static String getTypeForComponentId(Class<? extends TOSCAComponentId> idClass) {
146                 return Util.getEverythingBetweenTheLastDotAndBeforeId(idClass);
147         }
148         
149         /**
150          * Returns the root path fragment for the given
151          * AbstractComponentIntanceResource
152          * 
153          * With trailing slash
154          * 
155          * @return [ComponentName]s/
156          */
157         public static String getRootPathFragment(Class<? extends TOSCAComponentId> idClass) {
158                 // quick handling of imports special case
159                 // in the package naming, all other component instances have a this intermediate location, but not in the URLs
160                 // The package handling is in {@link org.eclipse.winery.repository.Utils.getIntermediateLocationStringForType(String, String)}
161                 String res;
162                 if (GenericImportId.class.isAssignableFrom(idClass)) {
163                         // this fires if idClass is a sub class from ImportCollectionId
164                         // special treatment for imports
165                         res = "imports/";
166                         if (XSDImportId.class.isAssignableFrom(idClass)) {
167                                 res = res + "http%3A%2F%2Fwww.w3.org%2F2001%2FXMLSchema/";
168                         } else {
169                                 throw new IllegalStateException("Not possible to determine local storage for generic imports class");
170                         }
171                         // we have the complete root path fragment
172                         return res;
173                 } else {
174                         res = "";
175                 }
176                 res = res + Util.getTypeForComponentId(idClass);
177                 res = res.toLowerCase();
178                 res = res + "s";
179                 res = res + "/";
180                 return res;
181         }
182         
183         /**
184          * Just calls @link{qname2href}
185          * 
186          * Introduced because of JSP error
187          * "The method qname2href(String, Class<? extends TExtensibleElements>, QName) in the type Util is not applicable for the arguments (String, Class<TNodeType>, QName, String)"
188          */
189         public static String qname2hrefWithName(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname, String name) {
190                 return Util.qname2href(repositoryUrl, element, qname, name);
191         }
192         
193         /**
194          * 
195          * @param repositoryUrl the URL to the repository
196          * @param element the element directly nested below a definitions element in
197          *            XML
198          * @param qname the QName of the element
199          * @param name (optional) if not null, the name to display as text in the
200          *            reference. Default: localName of the QName
201          * @return an <code>a</code> HTML element pointing to the given id
202          */
203         public static String qname2href(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname, String name) {
204                 if (StringUtils.isEmpty(repositoryUrl)) {
205                         throw new IllegalArgumentException("Repository URL must not be empty.");
206                 }
207                 if (element == null) {
208                         throw new IllegalArgumentException("Element class must not be null.");
209                 }
210                 if (qname == null) {
211                         return "(none)";
212                 }
213                 
214                 String absoluteURL = repositoryUrl + "/" + Util.getURLpathFragmentForCollection(element) + "/" + Util.DoubleURLencode(qname.getNamespaceURI()) + "/" + Util.DoubleURLencode(qname.getLocalPart());
215                 
216                 if (name == null) {
217                         // fallback if no name is given
218                         name = qname.getLocalPart();
219                 }
220                 // sanitize name
221                 name = Functions.escapeXml(name);
222                 
223                 String res = "<a target=\"_blank\" data-qname=\"" + qname + "\" href=\"" + absoluteURL + "\">" + name + "</a>";
224                 return res;
225         }
226         
227         /**
228          * 
229          * @param repositoryUrl the URL to the repository
230          * @param element the element directly nested below a definitions element in
231          *            XML
232          * @param qname the QName of the element
233          * @return an <code>a</code> HTML element pointing to the given id
234          */
235         public static String qname2href(String repositoryUrl, Class<? extends TExtensibleElements> element, QName qname) {
236                 return Util.qname2href(repositoryUrl, element, qname, null);
237         }
238         
239         /**
240          * Returns a visual rendering of minInstances
241          * 
242          * @param minInstances the value to render
243          */
244         public static String renderMinInstances(Integer minInstances) {
245                 if ((minInstances == null) || (minInstances == 1)) {
246                         // == null: default value: display nothing -- *never* happens:
247                         // the function *always* returns 1 even, if no explicit value is set. Therefore, we also display "" if the default value 1 is set
248                         return "";
249                 } else {
250                         return Integer.toString(minInstances);
251                 }
252         }
253         
254         /**
255          * Returns a visual rendering of maxInstances
256          * 
257          * @param maxInstances the value to render
258          */
259         public static String renderMaxInstances(String maxInstances) {
260                 if ((maxInstances == null) || (maxInstances.equals("1"))) {
261                         // default value display nothing
262                         // "1" is returned even if no explicit value has been set.
263                         return "";
264                 } else if (maxInstances.equals("unbounded")) {
265                         return "&infin;";
266                 } else {
267                         // maxInstance is a plain integer
268                         // return as is
269                         return maxInstances;
270                 }
271         }
272         
273         /**
274          * @return the local name of a Class representing a TOSCA element
275          */
276         private static String getLocalName(@SuppressWarnings("rawtypes") Class clazz) {
277                 String localName = clazz.getName();
278                 // a class defined within another class is written as superclass$class. E.g., EntityTemplate$Properties
279                 // We use the real class name
280                 int pos = localName.lastIndexOf('$');
281                 if (pos == -1) {
282                         pos = localName.lastIndexOf('.');
283                 }
284                 localName = localName.substring(pos + 1);
285                 if (localName.equals("TDocumentation")) {
286                         // special case for documentation: the local name starts with a lower case letter
287                         localName = "documentation";
288                 } else if (localName.startsWith("T")) {
289                         localName = localName.substring(1);
290                 }
291                 return localName;
292         }
293         
294         public static <T extends Object> JAXBElement<T> getJAXBElement(Class<T> clazz, T obj) {
295                 String namespace = null;
296                 XmlRootElement xmlRootElement = clazz.getAnnotation(XmlRootElement.class);
297                 if (xmlRootElement != null) {
298                         namespace = xmlRootElement.namespace();
299                         if ("##default".equals(namespace)) {
300                                 XmlSchema xmlSchema = clazz.getPackage().getAnnotation(XmlSchema.class);
301                                 if (xmlSchema != null) {
302                                         namespace = xmlSchema.namespace();
303                                 } else {
304                                         // trigger default handling
305                                         namespace = null;
306                                 }
307                         }
308                 }
309                 if (namespace == null) {
310                         // fallback non-specified namespaces
311                         namespace = org.eclipse.winery.common.constants.Namespaces.TOSCA_NAMESPACE;
312                 }
313                 String localName = Util.getLocalName(clazz);
314                 QName qname = new QName(namespace, localName);
315                 JAXBElement<T> rootElement = new JAXBElement<T>(qname, clazz, obj);
316                 return rootElement;
317         }
318         
319         /**
320          * Method similar to {@link
321          * org.eclipse.winery.repository.Utils.getXMLAsString(Class, Object)}.
322          * 
323          * Differences:
324          * <ul>
325          * <li>XML processing instruction is not included in the header</li>
326          * <li>JAXBcontext is created at each call</li>
327          * </ul>
328          */
329         public static <T extends Object> String getXMLAsString(Class<T> clazz, T obj) throws Exception {
330                 // copied from Utils java, but we create an own JAXBcontext here
331                 // JAXBSupport cannot be used as this relies on a MockElement, which we do not want to factor out to winery.common
332                 
333                 JAXBContext context;
334                 try {
335                         // For winery classes, eventually the package+jaxb.index method could be better. See http://stackoverflow.com/a/3628525/873282
336                         // @formatter:off
337                         context = JAXBContext.newInstance(
338                                         TEntityType.class);
339                         // @formatter:on
340                 } catch (JAXBException e) {
341                         throw new IllegalStateException(e);
342                 }
343                 
344                 JAXBElement<T> rootElement = Util.getJAXBElement(clazz, obj);
345                 Marshaller m = context.createMarshaller();
346                 m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
347                 m.setProperty(Marshaller.JAXB_FRAGMENT, true);
348                 // m.setProperty("com.sun.xml.bind.namespacePrefixMapper", JAXBSupport.prefixMapper);
349                 
350                 StringWriter w = new StringWriter();
351                 try {
352                         m.marshal(rootElement, w);
353                 } catch (JAXBException e) {
354                         throw new IllegalStateException(e);
355                 }
356                 String res = w.toString();
357                 return res;
358         }
359         
360         public static String getXMLAsString(Element el) {
361                 TransformerFactory tf = TransformerFactory.newInstance();
362                 Transformer t;
363                 try {
364                         t = tf.newTransformer();
365                 } catch (TransformerConfigurationException e) {
366                         throw new IllegalStateException("Could not instantiate Transformer", e);
367                 }
368                 t.setOutputProperty(OutputKeys.INDENT, "yes");
369                 Source source = new DOMSource(el);
370                 ByteArrayOutputStream os = new ByteArrayOutputStream();
371                 Result target = new StreamResult(os);
372                 try {
373                         t.transform(source, target);
374                 } catch (TransformerException e) {
375                         Util.logger.debug(e.getMessage(), e);
376                         throw new IllegalStateException("Could not transform dom node to string", e);
377                 }
378                 return os.toString();
379         }
380         
381         /**
382          * Determines whether the instance belonging to the given id supports the
383          * "name" attribute. This cannot be done using the super class as the TOSCA
384          * specification treats that differently in the case of EntityTemplates
385          * 
386          * NOTE: The respective subclasses of AbstractComponentInstanceResource have
387          * to implement {@link org.eclipse.winery.repository.resources.IHasName}
388          * 
389          * @param id the id to test
390          * @return true if the TOSCA model class belonging to the given id supports
391          *         the method "getName()" in addition to "getId()"
392          */
393         public static boolean instanceSupportsNameAttribute(Class<? extends TOSCAComponentId> idClass) {
394                 if (ServiceTemplateId.class.isAssignableFrom(idClass)) {
395                         return true;
396                 } else if ((EntityTypeId.class.isAssignableFrom(idClass)) || (EntityTypeImplementationId.class.isAssignableFrom(idClass))) {
397                         // name is available, but no id attribute
398                         return false;
399                 } else if (GenericImportId.class.isAssignableFrom(idClass)) {
400                         return false;
401                 } else {
402                         assert (EntityTemplateId.class.isAssignableFrom(idClass));
403                         if (ArtifactTemplateId.class.isAssignableFrom(idClass)) {
404                                 return true;
405                         } else if (PolicyTemplateId.class.isAssignableFrom(idClass)) {
406                                 return true;
407                         } else {
408                                 throw new IllegalStateException("Unimplemented branch to determine if getName() exists");
409                         }
410                 }
411         }
412         
413         public static String getLastURIPart(String loc) {
414                 int posSlash = loc.lastIndexOf('/');
415                 String fileName = loc.substring(posSlash + 1);
416                 return fileName;
417         }
418         
419         /**
420          * Determines a color belonging to the given name
421          */
422         public static String getColor(String name) {
423                 int hash = name.hashCode();
424                 // trim to 3*8=24 bits
425                 hash = hash & 0xFFFFFF;
426                 // check if color is more than #F0F0F0, i.e., too light
427                 if (((hash & 0xF00000) >= 0xF00000) && (((hash & 0x00F000) >= 0x00F000) && ((hash & 0x0000F0) >= 0x0000F0))) {
428                         // set one high bit to zero for each channel. That makes the overall color darker
429                         hash = hash & 0xEFEFEF;
430                 }
431                 String colorStr = String.format("#%06x", hash);
432                 return colorStr;
433         }
434         
435         /**
436          * Determines the name of the CSS class used for relationshipTypes at
437          * nodeTemplateRenderer.tag
438          */
439         public static String makeCSSName(String namespace, String localName) {
440                 // according to http://stackoverflow.com/a/79022/873282 everything is allowed
441                 // However, {namespace}id does NOT work
442                 String res = namespace + "_" + localName;
443                 res = res.replaceAll("[^\\w\\d_]", "_");
444                 return res;
445         }
446         
447         /**
448          * @see {@link org.eclipse.winery.common.Util.makeCSSName(String, String)}
449          */
450         public static String makeCSSName(QName qname) {
451                 return Util.makeCSSName(qname.getNamespaceURI(), qname.getLocalPart());
452         }
453         
454         public static SortedMap<String, SortedSet<String>> convertQNameListToNamespaceToLocalNameList(List<QName> list) {
455                 SortedMap<String, SortedSet<String>> res = new TreeMap<>();
456                 for (QName qname : list) {
457                         SortedSet<String> localNameSet = res.get(qname.getNamespaceURI());
458                         if (localNameSet == null) {
459                                 localNameSet = new TreeSet<>();
460                                 res.put(qname.getNamespaceURI(), localNameSet);
461                         }
462                         localNameSet.add(qname.getLocalPart());
463                 }
464                 return res;
465         }
466         
467         public static String namespaceToJavaPackage(String namespace) {
468                 URI uri;
469                 try {
470                         uri = new URI(namespace);
471                 } catch (URISyntaxException e) {
472                         Util.logger.debug(e.getMessage(), e);
473                         return "uri.invalid";
474                 }
475                 StringBuilder sb = new StringBuilder();
476                 
477                 String host = uri.getHost();
478                 if (host != null) {
479                         Util.addReversed(sb, host, "\\.");
480                 }
481                 
482                 String path = uri.getPath();
483                 if (!path.equals("")) {
484                         if (path.startsWith("/")) {
485                                 // remove first slash
486                                 path = path.substring(1);
487                         }
488                         
489                         // and then handle the string
490                         Util.addAsIs(sb, path, "/");
491                 }
492                 
493                 // remove the final dot
494                 sb.replace(sb.length() - 1, sb.length(), "");
495                 
496                 return Util.cleanName(sb.toString());
497         }
498         
499         private static String cleanName(String s) {
500                 // TODO: Integrate with other name cleaning functions. "." should not be replaced as it is used as separator in the java package name
501                 // @formatter:off
502                 return s.replace(":", Util.FORBIDDEN_CHARACTER_REPLACEMENT)
503                                 .replace("/", Util.FORBIDDEN_CHARACTER_REPLACEMENT)
504                                 .replace(" ", Util.FORBIDDEN_CHARACTER_REPLACEMENT)
505                                 .replace("-", Util.FORBIDDEN_CHARACTER_REPLACEMENT);
506                 // @formatter:on
507         }
508         
509         
510         /*
511         * Valid chars: See
512         * <ul>
513         * <li>http://www.w3.org/TR/REC-xml-names/#NT-NCName</li>
514         * <li>http://www.w3.org/TR/REC-xml/#NT-Name</li>
515         * </ul>
516         */
517         // NameCharRange \u10000-\ueffff is not supported by Java
518         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]";
519         private static final String NCNameChar_RegExp = Util.NCNameStartChar_RegExp + "|[-\\.0-9\u00B7\u0300-\u036F\u203F-\u2040]";
520         private static final Pattern NCNameStartChar_Pattern = Pattern.compile(Util.NCNameStartChar_RegExp);
521         private static final Pattern NCNameChar_RegExp_Pattern = Pattern.compile(Util.NCNameChar_RegExp);
522         
523         
524         /**
525          * Removes all non-NCName characters from the given string and returns the
526          * result
527          * 
528          * This function should be consistent with
529          * org.eclipse.winery.common.Util.cleanName(String)
530          * 
531          * TODO: This method seems to be equal to {@link
532          * org.eclipse.winery.repository.Utils.createXMLidAsString(String)}. These
533          * methods should be merged.
534          * 
535          */
536         public static String makeNCName(String text) {
537                 if (StringUtils.isEmpty(text)) {
538                         return text;
539                 }
540                 
541                 StringBuffer res = new StringBuffer();
542                 
543                 // handle start
544                 String start = text.substring(0, 1);
545                 Matcher m = Util.NCNameStartChar_Pattern.matcher(start);
546                 if (m.matches()) {
547                         res.append(start);
548                 } else {
549                         // not a valid character
550                         res.append("_");
551                 }
552                 
553                 // handle remaining characters;
554                 for (int i = 1; i < text.length(); i++) {
555                         String s = text.substring(i, i + 1);
556                         m = Util.NCNameChar_RegExp_Pattern.matcher(s);
557                         if (m.matches()) {
558                                 res.append(s);
559                         } else {
560                                 // not a valid character
561                                 res.append("_");
562                         }
563                 }
564                 
565                 return res.toString();
566         }
567         
568         private static void addAsIs(StringBuilder sb, String s, String separator) {
569                 if (s.isEmpty()) {
570                         return;
571                 }
572                 String[] split = s.split(separator);
573                 for (int i = 0; i < split.length; i++) {
574                         sb.append(split[i]);
575                         sb.append(".");
576                 }
577         }
578         
579         private static void addReversed(StringBuilder sb, String s, String separator) {
580                 String[] split = s.split(separator);
581                 for (int i = split.length - 1; i >= 0; i--) {
582                         sb.append(split[i]);
583                         sb.append(".");
584                 }
585         }
586         
587         /**
588          * Bridge to client.getType(). Just calls client getType(), used by
589          * functions.tld.
590          * 
591          * We suppress compiler warnings as JSP 2.0 do not offer support for
592          * generics, but we're using JSP 2.0...
593          * 
594          * @param client the repository client to use
595          * @param qname the QName to resolve
596          * @param clazz the class the QName is describing
597          * @return {@inheritDoc}
598          */
599         @SuppressWarnings({"rawtypes", "unchecked"})
600         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) {
601                 return client.getType(qname, clazz);
602         }
603 }