/* * Copyright (c) 2010-2013 Evolveum * * 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 com.evolveum.midpoint.util; import java.util.*; import java.util.stream.Collectors; import javax.xml.namespace.QName; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.w3c.dom.Node; /** * * QName <-> URI conversion. * * Very simplistic but better than nothing. * * @author semancik */ public class QNameUtil { public static final Trace LOGGER = TraceManager.getTrace(QNameUtil.class); // TODO consider where to put all this undeclared-prefixes-things // Hopefully in 3.2 everything will find its place private static final String UNDECLARED_PREFIX_MARK = "__UNDECLARED__"; public static final char DEFAULT_QNAME_URI_SEPARATOR_CHAR = '#'; // Whether we want to tolerate undeclared XML prefixes in QNames // This is here only for backward compatibility with versions 3.0-3.1. // Will be set to false starting with 3.2 (MID-2191) private static boolean tolerateUndeclaredPrefixes = false; // ThreadLocal "safe mode" override for the above value (MID-2218) // This can be set to true for raw reads, allowing to manually fix broken objects private static ThreadLocal<Boolean> temporarilyTolerateUndeclaredPrefixes = new ThreadLocal<>(); public static String qNameToUri(QName qname) { return qNameToUri(qname, true); } public static String qNameToUri(QName qname, boolean unqualifiedStartsWithHash) { return qNameToUri(qname, unqualifiedStartsWithHash, DEFAULT_QNAME_URI_SEPARATOR_CHAR); } public static String qNameToUri(QName qname, boolean unqualifiedStartsWithHash, char separatorChar) { String qUri = qname.getNamespaceURI(); StringBuilder sb = new StringBuilder(qUri); // TODO: Check if there's already a fragment // e.g. http://foo/bar#baz if (!qUri.endsWith("#") && !qUri.endsWith("/")) { if (unqualifiedStartsWithHash || !qUri.isEmpty()) { sb.append(separatorChar); } } sb.append(qname.getLocalPart()); return sb.toString(); } public static QName uriToQName(String uri) { return uriToQName(uri, false); } public static boolean noNamespace(@NotNull QName name) { return StringUtils.isEmpty(name.getNamespaceURI()); } public static boolean hasNamespace(@NotNull QName name) { return !noNamespace(name); } public static QName unqualify(QName name) { return new QName(name.getLocalPart()); } public static QName qualifyIfNeeded(QName name, String defaultNamespace) { return hasNamespace(name) ? name : new QName(defaultNamespace, name.getLocalPart()); } public static <V> V getKey(@NotNull Map<QName, V> map, @NotNull QName key) { if (hasNamespace(key)) { return map.get(key); } List<Map.Entry<QName, V>> matching = map.entrySet().stream() .filter(e -> match(e.getKey(), key)) .collect(Collectors.toList()); if (matching.isEmpty()) { return null; } else if (matching.size() == 1) { return matching.get(0).getValue(); } else { throw new IllegalStateException("More than one matching value for key " + key + ": " + matching); } } // returns null if no change is requested public static String qualifyUriIfNeeded(String uri, String namespace) { if (StringUtils.isEmpty(namespace) || StringUtils.isEmpty(uri)) { return null; } QNameInfo info = uriToQNameInfo(uri, true); if (hasNamespace(info.name) || info.explicitEmptyNamespace) { return null; } else { return qNameToUri(new QName(namespace, info.name.getLocalPart())); } } @NotNull public static QName setNamespaceIfMissing(@NotNull QName name, @NotNull String namespace, @Nullable String prefix) { if (hasNamespace(name)) { return name; } else if (prefix == null) { return new QName(namespace, name.getLocalPart()); } else { return new QName(namespace, name.getLocalPart(), prefix); } } public static boolean matchUri(String uri1, String uri2) { if (java.util.Objects.equals(uri1, uri2)) { return true; } else if (uri1 == null || uri2 == null) { return false; } else { return match(uriToQName(uri1, true), uriToQName(uri2, true)); } } public static class QNameInfo { @NotNull public final QName name; public final boolean explicitEmptyNamespace; public QNameInfo(@NotNull QName name, boolean explicitEmptyNamespace) { this.name = name; this.explicitEmptyNamespace = explicitEmptyNamespace; } } public static QName uriToQName(String uri, boolean allowUnqualified) { return uriToQNameInfo(uri, allowUnqualified).name; } @NotNull public static QName uriToQName(String uri, String defaultNamespace) { QNameInfo info = uriToQNameInfo(uri, true); if (hasNamespace(info.name) || info.explicitEmptyNamespace || StringUtils.isEmpty(defaultNamespace)) { return info.name; } else { return new QName(defaultNamespace, info.name.getLocalPart()); } } @NotNull public static QNameInfo uriToQNameInfo(@NotNull String uri, boolean allowUnqualified) { Validate.notNull(uri); int index = uri.lastIndexOf("#"); if (index != -1) { String ns = uri.substring(0, index); String name = uri.substring(index+1); return new QNameInfo(new QName(ns, name), "".equals(ns)); } index = uri.lastIndexOf("/"); // TODO check if this is still in the path section, e.g. // if the matched slash is not a beginning of authority // section if (index != -1) { String ns = uri.substring(0, index); String name = uri.substring(index+1); return new QNameInfo(new QName(ns, name), "".equals(ns)); } if (allowUnqualified) { return new QNameInfo(new QName(uri), false); } else { throw new IllegalArgumentException("The URI (" + uri + ") does not contain slash character"); } } public static QName getNodeQName(Node node) { return new QName(node.getNamespaceURI(),node.getLocalName()); } public static boolean compareQName(QName qname, Node node) { return (qname.getNamespaceURI().equals(node.getNamespaceURI()) && qname.getLocalPart().equals(node.getLocalName())); } /** * Matching with considering wildcard namespace (null). */ public static boolean match(QName a, QName b) { return match(a, b, false); } // case insensitive is related to local parts public static boolean match(QName a, QName b, boolean caseInsensitive) { if (a == null && b == null) { return true; } if (a == null || b == null) { return false; } if (!caseInsensitive) { // traditional comparison if (StringUtils.isBlank(a.getNamespaceURI()) || StringUtils.isBlank(b.getNamespaceURI())) { return a.getLocalPart().equals(b.getLocalPart()); } else { return a.equals(b); } } else { // relaxed (case-insensitive) one if (!a.getLocalPart().equalsIgnoreCase(b.getLocalPart())) { return false; } if (StringUtils.isBlank(a.getNamespaceURI()) || StringUtils.isBlank(b.getNamespaceURI())) { return true; } else { return a.getNamespaceURI().equals(b.getNamespaceURI()); } } } /** * Matches QName with a URI representation. The URL may in fact be just the local * part. */ public static boolean matchWithUri(QName qname, String uri) { return match(qname, uriToQName(uri, true)); } public static QName resolveNs(QName a, Collection<QName> col){ if (col == null) { return null; } QName found = null; for (QName b: col) { if (match(a, b)) { if (found != null){ throw new IllegalStateException("Found more than one suitable qnames( "+ found + b + ") for attribute: " + a); } found = b; } } return found; } public static boolean matchAny(QName a, Collection<QName> col) { if (resolveNs(a, col) == null){ return false; } return true; // if (col == null) { // return false; // } // for (QName b: col) { // if (match(a, b)) { // return true; // } // } // return false; } public static Collection<QName> createCollection(QName... qnames) { return Arrays.asList(qnames); } public static QName nullNamespace(QName qname) { return new QName(null, qname.getLocalPart(), qname.getPrefix()); } public static boolean isUnqualified(QName targetTypeQName) { return StringUtils.isBlank(targetTypeQName.getNamespaceURI()); } public static boolean isTolerateUndeclaredPrefixes() { return tolerateUndeclaredPrefixes; } public static void setTolerateUndeclaredPrefixes(boolean value) { tolerateUndeclaredPrefixes = value; } public static void setTemporarilyTolerateUndeclaredPrefixes(Boolean value) { temporarilyTolerateUndeclaredPrefixes.set(value); } public static void reportUndeclaredNamespacePrefix(String prefix, String context) { if (tolerateUndeclaredPrefixes || (temporarilyTolerateUndeclaredPrefixes != null && Boolean.TRUE.equals(temporarilyTolerateUndeclaredPrefixes.get()))) { LOGGER.error("Undeclared namespace prefix '" + prefix+"' in '"+context+"'."); } else { throw new IllegalArgumentException("Undeclared namespace prefix '"+prefix+"' in '"+context+"'"); } } // @pre namespacePrefix != null public static String markPrefixAsUndeclared(String namespacePrefix) { if (namespacePrefix.startsWith(UNDECLARED_PREFIX_MARK)) { return namespacePrefix; } else { return UNDECLARED_PREFIX_MARK + namespacePrefix; } } public static boolean isPrefixUndeclared(String namespacePrefix) { return namespacePrefix != null && namespacePrefix.startsWith(UNDECLARED_PREFIX_MARK); } public static boolean isUri(String string) { if (string == null) { return false; } return string.matches("^\\w+:.*"); } public static String getLocalPart(QName name) { return name != null ? name.getLocalPart() : null; } public static boolean contains(Collection<QName> col, QName qname) { return col != null && col.stream().anyMatch(e -> match(e, qname)); } public static boolean remove(Collection<QName> col, QName qname) { return col != null && col.removeIf(e -> match(e, qname)); } public static String escapeElementName(String name) { if (name == null || name.isEmpty()) { return name; // suspicious but that's not our business } StringBuilder sb = new StringBuilder(); for (int i = 0; i < name.length(); i++) { char ch = name.charAt(i); if (allowed(ch, i==0)) { sb.append(ch); } else { sb.append("_x").append(Long.toHexString(ch)); } } return sb.toString(); } // TODO fix this method if necessary // see https://www.w3.org/TR/REC-xml/#NT-NameChar (JSON and YAML can - very probably - use any characters for "element" names) private static boolean allowed(char ch, boolean atStart) { return Character.isLetter(ch) || ch == '_' || (!atStart && (Character.isDigit(ch) || ch == '.' || ch == '-')); } }