| /* |
| * 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.namepath.impl; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Map; |
| |
| import javax.jcr.RepositoryException; |
| |
| import org.apache.jackrabbit.oak.commons.PathUtils; |
| import org.apache.jackrabbit.oak.namepath.JcrPathParser; |
| import org.apache.jackrabbit.oak.namepath.JcrPathParser.Listener; |
| import org.apache.jackrabbit.oak.namepath.NameMapper; |
| import org.apache.jackrabbit.oak.namepath.NamePathMapper; |
| import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager; |
| import org.jetbrains.annotations.NotNull; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * TODO document |
| */ |
| public class NamePathMapperImpl implements NamePathMapper { |
| |
| /** |
| * logger instance |
| */ |
| private static final Logger log = LoggerFactory.getLogger(NamePathMapperImpl.class); |
| |
| private final NameMapper nameMapper; |
| private final IdentifierManager idManager; |
| |
| public NamePathMapperImpl(NameMapper nameMapper) { |
| this.nameMapper = nameMapper; |
| this.idManager = null; |
| } |
| |
| public NamePathMapperImpl(NameMapper nameMapper, IdentifierManager idManager) { |
| this.nameMapper = nameMapper; |
| this.idManager = idManager; |
| } |
| |
| //---------------------------------------------------------< NameMapper >--- |
| @Override |
| public String getOakNameOrNull(@NotNull String jcrName) { |
| return nameMapper.getOakNameOrNull(jcrName); |
| } |
| |
| @NotNull |
| @Override |
| public String getOakName(@NotNull String jcrName) throws RepositoryException { |
| return nameMapper.getOakName(jcrName); |
| } |
| |
| @NotNull |
| @Override |
| public String getJcrName(@NotNull String oakName) { |
| return nameMapper.getJcrName(oakName); |
| } |
| |
| @Override @NotNull |
| public Map<String, String> getSessionLocalMappings() { |
| return nameMapper.getSessionLocalMappings(); |
| } |
| |
| //---------------------------------------------------------< PathMapper >--- |
| @Override |
| public String getOakPath(String jcrPath) { |
| if (!needsFullMapping(jcrPath)) { |
| return jcrPath; |
| } |
| |
| int length = jcrPath.length(); |
| |
| // identifier path? |
| if (length > 0 && jcrPath.charAt(0) == '[') { |
| if (jcrPath.charAt(length - 1) != ']') { |
| log.debug("Could not parse path " + jcrPath + ": unterminated identifier"); |
| return null; |
| } |
| if (this.idManager == null) { |
| log.debug("Could not parse path " + jcrPath + ": could not resolve identifier"); |
| return null; |
| } |
| return this.idManager.getPath(jcrPath.substring(1, length - 1)); |
| } |
| |
| final StringBuilder parseErrors = new StringBuilder(); |
| |
| PathListener listener = new PathListener() { |
| @Override |
| public void error(String message) { |
| parseErrors.append(message); |
| } |
| |
| @Override |
| public boolean name(String name, int index) { |
| if (index < 0) { |
| error("invalid index: " + index); |
| return false; |
| } |
| |
| String oakName = nameMapper.getOakNameOrNull(name); |
| if (oakName == null) { |
| error("Invalid name: " + name); |
| return false; |
| } |
| if (index > 1) { |
| oakName += "[" + index + ']'; |
| } |
| elements.add(oakName); |
| return true; |
| } |
| }; |
| |
| JcrPathParser.parse(jcrPath, listener); |
| if (parseErrors.length() != 0) { |
| log.debug("Could not parse path " + jcrPath + ": " + parseErrors.toString()); |
| return null; |
| } |
| |
| // Empty path maps to "" |
| if (listener.elements.isEmpty()) { |
| return ""; |
| } |
| |
| StringBuilder oakPath = new StringBuilder(); |
| for (String element : listener.elements) { |
| if (element.isEmpty()) { |
| // root |
| oakPath.append('/'); |
| } |
| else { |
| oakPath.append(element); |
| oakPath.append('/'); |
| } |
| } |
| |
| // root path is special-cased early on so it does not need to |
| // be considered here |
| oakPath.deleteCharAt(oakPath.length() - 1); |
| return oakPath.toString(); |
| } |
| |
| @Override |
| @NotNull |
| public String getJcrPath(final String oakPath) { |
| if ("/".equals(oakPath)) { |
| // avoid the need to special case the root path later on |
| return "/"; |
| } else if (oakPath.isEmpty()) { |
| // empty path: map to "." |
| return "."; |
| } else if (nameMapper.getSessionLocalMappings().isEmpty()) { |
| // no local namespace mappings |
| return oakPath; |
| } |
| |
| PathListener listener = new PathListener() { |
| @Override |
| public boolean current() { |
| // nothing to do here |
| return false; |
| } |
| |
| @Override |
| public void error(String message) { |
| throw new IllegalArgumentException(message); |
| } |
| |
| @Override |
| public boolean name(String name, int index) { |
| String p = nameMapper.getJcrName(name); |
| if (index == 0) { |
| elements.add(p); |
| } else { |
| elements.add(p + '[' + index + ']'); |
| } |
| return true; |
| } |
| }; |
| |
| JcrPathParser.parse(oakPath, listener); |
| |
| StringBuilder jcrPath = new StringBuilder(); |
| for (String element : listener.elements) { |
| if (element.isEmpty()) { |
| // root |
| jcrPath.append('/'); |
| } |
| else { |
| jcrPath.append(element); |
| jcrPath.append('/'); |
| } |
| } |
| |
| jcrPath.deleteCharAt(jcrPath.length() - 1); |
| return jcrPath.toString(); |
| } |
| |
| /** |
| * Checks if the given path needs to be fully parsed to apply namespace |
| * mappings or to validate its syntax. If the given path is "simple", i.e. |
| * it doesn't contain any complex constructs, and there are no local |
| * namespace remappings, it's possible to skip the full path parsing |
| * and simply use the JCR path string as-is as an Oak path. |
| * |
| * @param path JCR path |
| * @return {@code true} if the path needs to be fully parsed, |
| * {@code false} if not |
| */ |
| private boolean needsFullMapping(String path) { |
| int length = path.length(); |
| if (length == 0) { |
| return true; |
| } |
| |
| int slash = -1; // index of the last slash in the path |
| int colon = -1; // index of the last colon in the path |
| |
| switch (path.charAt(0)) { |
| case '{': // possibly an expanded name |
| case '[': // starts with an identifier |
| case '.': // possibly "." or ".." |
| case ':': // colon as the first character |
| return true; |
| case '/': |
| if (length == 1) { |
| return false; // the root path |
| } |
| slash = 0; |
| break; |
| } |
| |
| for (int i = 1; i < length; i++) { |
| switch (path.charAt(i)) { |
| case '{': // possibly an expanded name |
| case '[': // possibly an index |
| case ']': // illegal character if not part of index |
| case '|': // illegal character |
| case '*': // illegal character |
| return true; |
| case '.': |
| if (i == slash + 1) { |
| return true; // possibly "." or ".." |
| } |
| break; |
| case ':': |
| if (i == slash + 1 // "x/:y" |
| || i == colon + i // "x::y" |
| || colon > slash // "x:y:z" |
| || i + 1 == length) { // "x:" |
| return true; |
| } |
| colon = i; |
| break; |
| case '/': |
| if (i == slash + 1 // "x//y" |
| || i == colon + i // "x:/y" |
| || i + 1 == length) { // "x/" |
| return true; |
| } |
| slash = i; |
| break; |
| } |
| } |
| |
| return colon != -1 && !nameMapper.getSessionLocalMappings().isEmpty(); |
| } |
| |
| //------------------------------------------------------------< PathListener >--- |
| |
| private abstract static class PathListener implements Listener { |
| final List<String> elements = new ArrayList<String>(); |
| |
| @Override |
| public boolean root() { |
| if (!elements.isEmpty()) { |
| error("/ on non-empty path"); |
| return false; |
| } |
| elements.add(""); |
| return true; |
| } |
| |
| @Override |
| public boolean current() { |
| // nothing to do here |
| return true; |
| } |
| |
| @Override |
| public boolean parent() { |
| int prevIdx = elements.size() - 1; |
| String prevElem = prevIdx >= 0 ? elements.get(prevIdx) : null; |
| |
| if (prevElem == null || PathUtils.denotesParent(prevElem)) { |
| elements.add(".."); |
| return true; |
| } |
| if (prevElem.isEmpty()) { |
| error("Absolute path escapes root"); |
| return false; |
| } |
| |
| elements.remove(prevIdx); |
| return true; |
| } |
| } |
| |
| } |