| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.jackrabbit.oak.plugins.name; |
| |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| |
| import org.apache.jackrabbit.oak.api.PropertyState; |
| import org.apache.jackrabbit.oak.api.Tree; |
| import org.apache.jackrabbit.oak.api.Type; |
| import org.apache.jackrabbit.oak.spi.namespace.NamespaceConstants; |
| import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants; |
| import org.apache.jackrabbit.oak.spi.state.NodeBuilder; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.common.collect.Maps.newConcurrentMap; |
| import static com.google.common.collect.Maps.newHashMap; |
| import static com.google.common.collect.Sets.newHashSet; |
| import static javax.jcr.NamespaceRegistry.NAMESPACE_JCR; |
| import static javax.jcr.NamespaceRegistry.NAMESPACE_MIX; |
| import static javax.jcr.NamespaceRegistry.NAMESPACE_NT; |
| import static javax.jcr.NamespaceRegistry.NAMESPACE_XML; |
| import static javax.jcr.NamespaceRegistry.PREFIX_JCR; |
| import static javax.jcr.NamespaceRegistry.PREFIX_MIX; |
| import static javax.jcr.NamespaceRegistry.PREFIX_NT; |
| import static javax.jcr.NamespaceRegistry.PREFIX_XML; |
| import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE; |
| import static org.apache.jackrabbit.JcrConstants.JCR_SYSTEM; |
| import static org.apache.jackrabbit.oak.api.Type.NAME; |
| import static org.apache.jackrabbit.oak.api.Type.STRING; |
| import static org.apache.jackrabbit.util.Text.escapeIllegalJcrChars; |
| |
| /** |
| * Internal static utility class for managing the persisted namespace registry. |
| */ |
| public class Namespaces implements NamespaceConstants { |
| |
| /** |
| * Global cache of encoded URIs. |
| */ |
| private static final Map<String, String> ENCODED_URIS = newConcurrentMap(); |
| |
| /** |
| * By default, item names with non space whitespace chars are not allowed. |
| * However initial Oak release did allowed that and this flag is provided |
| * to revert back to old behaviour if required for some case temporarily |
| */ |
| private static final boolean allowOtherWhitespaceChars = Boolean.getBoolean("oak.allowOtherWhitespaceChars"); |
| |
| /** |
| * By default, item names with control characters are not allowed. |
| * Oak releases prior to 1.10 allowed these (in conflict with the JCR |
| * specification), so if required the check can be turned off. |
| * See OAK-7208. |
| */ |
| private static final boolean allowOtherControlChars = Boolean.getBoolean("oak.allowOtherControlChars"); |
| |
| /** |
| * By default, item names with non-ASCII whitespace characters are allowed. |
| * Oak releases prior to 1.10 disallowed these, so if required the check can |
| * be turned on again. See OAK-4857. |
| */ |
| private static final boolean disallowNonASCIIWhitespaceChars = Boolean.getBoolean("oak.disallowNonASCIIWhitespaceChars"); |
| |
| private Namespaces() { |
| } |
| |
| public static void setupNamespaces(NodeBuilder system) { |
| if (!system.hasChildNode(REP_NAMESPACES)) { |
| NodeBuilder namespaces = createStandardMappings(system); |
| buildIndexNode(namespaces); // index node for faster lookup |
| } |
| } |
| |
| public static NodeBuilder createStandardMappings(NodeBuilder system) { |
| checkState(!system.hasChildNode(REP_NAMESPACES)); |
| |
| NodeBuilder namespaces = system.setChildNode(REP_NAMESPACES); |
| namespaces.setProperty(JCR_PRIMARYTYPE, NodeTypeConstants.NT_REP_UNSTRUCTURED, NAME); |
| |
| // Standard namespace specified by JCR (default one not included) |
| namespaces.setProperty(PREFIX_JCR, NAMESPACE_JCR); |
| namespaces.setProperty(PREFIX_NT, NAMESPACE_NT); |
| namespaces.setProperty(PREFIX_MIX, NAMESPACE_MIX); |
| namespaces.setProperty(PREFIX_XML, NAMESPACE_XML); |
| |
| // Namespace included in Jackrabbit 2.x |
| namespaces.setProperty(PREFIX_SV, NAMESPACE_SV); |
| namespaces.setProperty(PREFIX_REP, NAMESPACE_REP); |
| |
| // Oak Namespace |
| namespaces.setProperty(PREFIX_OAK, NAMESPACE_OAK); |
| |
| return namespaces; |
| } |
| |
| public static String addCustomMapping( |
| NodeBuilder namespaces, String uri, String prefixHint) { |
| // first look for an existing mapping for the given URI |
| for (PropertyState property : namespaces.getProperties()) { |
| if (property.getType() == STRING) { |
| String prefix = property.getName(); |
| if (isValidPrefix(prefix) |
| && uri.equals(property.getValue(STRING))) { |
| return prefix; |
| } |
| } |
| } |
| |
| // no existing mapping found for the URI, make sure prefix is unique |
| String prefix = prefixHint; |
| int iteration = 1; |
| while (namespaces.hasProperty(prefix)) { |
| prefix = prefixHint + ++iteration; |
| } |
| |
| // add the new mapping with its unique prefix |
| namespaces.setProperty(prefix, uri); |
| return prefix; |
| } |
| |
| public static void buildIndexNode(NodeBuilder namespaces) { |
| // initialize prefix and URI sets with the defaults namespace |
| // that's not stored along with the other mappings |
| Set<String> prefixes = newHashSet(""); |
| Set<String> uris = newHashSet(""); |
| Map<String, String> nsmap = collectNamespaces(namespaces.getProperties()); |
| prefixes.addAll(nsmap.keySet()); |
| uris.addAll(nsmap.values()); |
| |
| NodeBuilder data = namespaces.setChildNode(REP_NSDATA); |
| data.setProperty(JCR_PRIMARYTYPE, NodeTypeConstants.NT_REP_UNSTRUCTURED, Type.NAME); |
| data.setProperty(REP_PREFIXES, prefixes, Type.STRINGS); |
| data.setProperty(REP_URIS, uris, Type.STRINGS); |
| for (Entry<String, String> e : nsmap.entrySet()) { |
| // persist as reverse index |
| data.setProperty(encodeUri(e.getValue()), e.getKey()); |
| } |
| } |
| |
| private static Tree getNamespaceTree(Tree root) { |
| return root.getChild(JCR_SYSTEM).getChild(REP_NAMESPACES); |
| } |
| |
| public static Map<String, String> getNamespaceMap(Tree root) { |
| Map<String, String> map = collectNamespaces(getNamespaceTree(root).getProperties()); |
| map.put("", ""); // default namespace, not included in tree |
| return map; |
| } |
| |
| static Map<String, String> collectNamespaces(Iterable<? extends PropertyState> properties) { |
| Map<String, String> map = newHashMap(); |
| for (PropertyState property : properties) { |
| String prefix = property.getName(); |
| if (STRING.equals(property.getType()) && isValidPrefix(prefix)) { |
| map.put(prefix, property.getValue(STRING)); |
| } |
| } |
| return map; |
| } |
| |
| public static String getNamespacePrefix(Tree root, String uri) { |
| if (uri.isEmpty()) { |
| return uri; |
| } |
| |
| Tree nsdata = getNamespaceTree(root).getChild(REP_NSDATA); |
| PropertyState ps = nsdata.getProperty(encodeUri(uri)); |
| if (ps != null) { |
| return ps.getValue(STRING); |
| } |
| |
| return null; |
| } |
| |
| public static String getNamespaceURI(Tree root, String prefix) { |
| if (prefix.isEmpty()) { |
| return prefix; |
| } |
| |
| if (isValidPrefix(prefix)) { |
| PropertyState property = getNamespaceTree(root).getProperty(prefix); |
| if (property != null && property.getType() == STRING) { |
| return property.getValue(STRING); |
| } |
| } |
| |
| return null; |
| } |
| |
| // utils |
| |
| /** |
| * encodes the uri value to be used as a property |
| * |
| * @param uri |
| * @return encoded uri |
| */ |
| public static String encodeUri(String uri) { |
| String encoded = ENCODED_URIS.get(uri); |
| if (encoded == null) { |
| encoded = escapeIllegalJcrChars(uri); |
| if (ENCODED_URIS.size() > 1000) { |
| ENCODED_URIS.clear(); // prevents DoS attacks |
| } |
| ENCODED_URIS.put(uri, encoded); |
| } |
| return encoded; |
| } |
| |
| // validation |
| |
| public static boolean isValidPrefix(String prefix) { |
| // TODO: Other prefix rules? |
| return prefix.indexOf(':') == -1; |
| } |
| |
| public static boolean isValidLocalName(String local) { |
| if (local.isEmpty() || ".".equals(local) || "..".equals(local)) { |
| return false; |
| } |
| |
| for (int i = 0; i < local.length(); i++) { |
| |
| char ch = local.charAt(i); |
| |
| boolean spaceChar; |
| if (disallowNonASCIIWhitespaceChars) { |
| // behavior before OAK-4857 was fixed |
| spaceChar = allowOtherWhitespaceChars ? Character.isSpaceChar(ch) : Character.isWhitespace(ch); |
| } else { |
| // disallow just leading and trailing ' ', plus CR, LF and TAB |
| spaceChar = ch == ' ' || ch == 0x9 || ch == 0xa || ch == 0xd; |
| } |
| |
| if (spaceChar) { |
| if (i == 0) { |
| return false; // leading whitespace |
| } else if (i == local.length() - 1) { |
| return false; // trailing whitespace |
| } else if (ch != ' ') { |
| return false; // only spaces are allowed as whitespace |
| } |
| } else if ("/:[]|*".indexOf(ch) != -1) { // TODO: XMLChar check for unpaired surrogates |
| return false; // invalid name character |
| } else if (!allowOtherControlChars && ch >= 0 && ch < 32 && (ch != 9 && ch != 0xa && ch != 0xd)) { |
| // https://www.w3.org/TR/xml/#NT-Char - disallowed control chars |
| return false; |
| } |
| } |
| |
| // TODO: Other name rules? |
| return true; |
| } |
| |
| } |