/* * 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.server.remoting.davex; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Set; import javax.jcr.Item; import javax.jcr.ItemNotFoundException; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.Workspace; import javax.jcr.nodetype.NodeType; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.apache.jackrabbit.JcrConstants; import org.apache.jackrabbit.server.util.RequestData; import org.apache.jackrabbit.util.Text; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavLocatorFactory; import org.apache.jackrabbit.webdav.DavMethods; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.WebdavRequest; import org.apache.jackrabbit.webdav.WebdavResponse; import org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet; import org.apache.jackrabbit.webdav.jcr.JcrDavException; import org.apache.jackrabbit.webdav.jcr.JcrDavSession; import org.apache.jackrabbit.webdav.jcr.transaction.TxLockManagerImpl; import org.apache.jackrabbit.webdav.observation.SubscriptionManager; import org.apache.jackrabbit.webdav.version.DeltaVConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>JcrRemotingServlet</code> is an extended version of the * {@link org.apache.jackrabbit.webdav.jcr.JCRWebdavServerServlet JCR Remoting Servlet} * that provides improved * <ul> * <li><a href="#bread">Batch read</a></li> * <!-- <li><a href="#mread">Multi read</a></li> --> * <li><a href="#bwrite">Batch write</a></li> * </ul> * functionality and supports cross workspace copy and cloning. * * <h3><a name="bread">Batch Read</a></h3> * * Upon RepositoryService.getItemInfos a JSON object is composed containing * the information for the requested node and its child items up to a * specified or configuration determined depth. * <p> * Batch read is triggered by adding a '.json' extension to the resource href. * Optionally the client may explicitly specify the desired batch read depth * by appending '.depth.json' extension. If no json extension is present the * GET request is processed by the base servlet. * <p> * The JSON writer applies the following rules: * * <pre> * - Nodes are represented as JSON objects. * * - Each Node has its properties included as JSON key/value pairs. * * - Single valued Properties are simple key/value pairs. * * - Multi valued Properties are represented as JSON array. * * - Each Node has its child nodes included as long a maximal depths is not reached. * * - Nodes without any child nodes get a special JSON member named * ::NodeIteratorSize, whose value is zero. * * - If the maximal depth is reached only name, index and unique id of the * direct child are included (incomplete node info). In order to obtain * the complete information the client sends another GET with .json extension. * </pre> * * Same name sibling nodes and properties whose type cannot be unambiguously be * extracted from the JSON on the client side need some special handling: * * <pre> * - Node with index > 1, get a JSON key consisting of * Node.getName() + "[" + Node.getIndex() + "]" * * - Binary Property * JSON value = length of the JCR value. * The JCR value must be retrieved separately. * * - Name, Path, Reference and Date Property * The JSON member representing the Property (name, value) is preceded by a * special member consisting of * JSON key = ":" + Property.getName() * JSON value = PropertyType.nameFromValue(Property.getType()) * * - Multi valued properties with Property.getValues().length == 0 will be * treated as special property types above (extra property indicating the * type of the property). * * - Double Property * JSON value must not have any trailing ".0" removed. * </pre> * * <h3><a name="mread">Multi Read</a></h3> * <p> * Since Jackrabbit 2.3.6 it is also possible to request multiple subtrees * in a single request. This is done by adding one or more ":include" * parameters to a batch read request describe above. These extra parameters * specify the (relative) paths of all the nodes to be included in the * response. The response is a JSON object whose "nodes" property contains * all the selected nodes keyed by path. Missing nodes are not included in * the response. Each included node is serialized as defined above for * <a href="#bread">batch read</a>. * <p> * Example: * <pre> * $ curl 'http://.../parent.json?:path=child1&:path=child2' * {"nodes":{"/parent/child1":{...},"/parent/child2":{...}}} * </pre> * * <h3><a name="bwrite">Batch Write</a></h3> * * The complete SPI Batch is sent to the server in a single request, currently a * POST request containing a custom ":diff" parameter. * <br> * <i>NOTE</i> that this is targeted to be replaced by a PATCH request. * * <h4>Diff format</h4> * * The diff parameter currently consists of JSON-like key-value pairs with the * following special requirements: * * <pre> * diff ::= members * members ::= pair | pairs * pair ::= key " : " value * pairs ::= pair line-end pair | pair line-end pairs * line-end ::= "\r\n" | "\n" | "\r" * key ::= diffchar path * diffchar ::= "+" | "^" | "-" | ">" * path ::= abspath | relpath * abspath ::= * absolute path to an item * * relpath ::= * relpath from item at request URI to an item * * value ::= value+ | value- | value^ | value> * value+ ::= * a JSON object * * value- ::= "" * value^ ::= * any JSON value except JSON object * * value> ::= path | path "#before" | path "#after" | "#first" | "#last" * </pre> * * In other words: * <ul> * <li>diff consists of one or more key-value pair(s)</li> * <li>key must start with a diffchar followed by a rel. or abs. item path</li> * <li>diffchar being any of "+", "^", "-" or ">" representing the transient * item modifications as follows * <pre> * "+" addNode * "^" setProperty / setValue / removeProperty * "-" remove Item * ">" move / reorder Nodes * </pre> * </li> * <li>key must be separated from the value by a ":" surrounded by whitespace.</li> * <li>two pairs must be separated by a line end</li> * <li>the format of the value depends on the diffchar</li> * <li>for moving around node the value must consist of a abs. or rel. path. * in contrast reordering of existing nodes is achieved by appending a trailing * order position hint (#first, #last, #before or #after)</li> * </ul> * * <i>NOTE</i> the following special handling of JCR properties of type * Binary, Name, Path, Date and Reference: * <ul> * <li>the JSON value must be missing</li> * <li>the POST request is expected to contain extra multipart(s) or request * parameter(s) for the property value(s)</li> * <li>the content type of the extra parts/params must reflect the property * type:"jcr-value/" + PropertyType.nameFromValue(Property.getType).toLowerCase()</li> * </ul> * * @see <a href="http://www.json.org/">www.json.org</a> for the definition of * JSON object and JSON value. */ public abstract class JcrRemotingServlet extends JCRWebdavServerServlet { private static Logger log = LoggerFactory.getLogger(JcrRemotingServlet.class); /** * the home init parameter. other relative filesystem paths are * relative to this location. */ public static final String INIT_PARAM_HOME = "home"; /** * the 'temp-directory' init parameter */ public static final String INIT_PARAM_TMP_DIRECTORY = "temp-directory"; /** * temp-dir attribute to be set to the servlet-context */ public static final String ATTR_TMP_DIRECTORY = "remoting-servlet.tmpdir"; /** * the 'temp-directory' init parameter */ public static final String INIT_PARAM_BATCHREAD_CONFIG = "batchread-config"; /** * the 'protectedhandlers-config' init paramter. this parameter contains the XML * configuration file for protected item remove handlers. */ public static final String INIT_PARAM_PROTECTED_HANDLERS_CONFIG = "protectedhandlers-config"; private static final String PARAM_DIFF = ":diff"; private static final String PARAM_COPY = ":copy"; private static final String PARAM_CLONE = ":clone"; private static final String PARAM_INCLUDE = ":include"; private BatchReadConfig brConfig; private ProtectedRemoveManager protectedRemoveManager; @Override public void init() throws ServletException { super.init(); brConfig = new BatchReadConfig(); String brConfigParam = getServletConfig().getInitParameter(INIT_PARAM_BATCHREAD_CONFIG); if (brConfigParam == null) { // TODO: define default values. log.debug("batchread-config missing -> initialize defaults."); brConfig.setDepth("nt:file", BatchReadConfig.DEPTH_INFINITE); brConfig.setDefaultDepth(5); } else { try { InputStream in = getServletContext().getResourceAsStream(brConfigParam); if (in != null) { brConfig.load(in); } } catch (IOException e) { log.debug("Unable to build BatchReadConfig from " + brConfigParam + "."); } } String protectedHandlerConfig = getServletConfig().getInitParameter(INIT_PARAM_PROTECTED_HANDLERS_CONFIG); InputStream in = null; try { in = getServletContext().getResourceAsStream(protectedHandlerConfig); if (in != null){ protectedRemoveManager = new ProtectedRemoveManager(); protectedRemoveManager.load(in); } else { //Config might be direct class implementation protectedRemoveManager = new ProtectedRemoveManager(protectedHandlerConfig); } } catch (IOException e) { log.debug("Unable to create ProtectedRemoveManager from " + protectedHandlerConfig , e); } finally{ if (in != null){ try { in.close(); } catch (IOException ignore) { } } } // Determine the configured location for temporary files used when // processing file uploads. Since JCR-3029 the default is the // standard java.io.tmpdir location, but the presence of explicit // configuration parameters restores the original behavior. File tmp = null; ServletConfig config = getServletConfig(); String paramHome = config.getInitParameter(INIT_PARAM_HOME); String paramTemp = config.getInitParameter(INIT_PARAM_TMP_DIRECTORY); if (paramHome != null || paramTemp != null) { if (paramHome == null) { log.debug("Missing init-param " + INIT_PARAM_HOME + ". Using default: 'jackrabbit'"); paramHome = "jackrabbit"; } else if (paramTemp == null) { log.debug("Missing init-param " + INIT_PARAM_TMP_DIRECTORY + ". Using default: 'tmp'"); paramTemp = "tmp"; } tmp = new File(paramHome, paramTemp); try { tmp = tmp.getCanonicalFile(); tmp.mkdirs(); log.debug(" temp-directory = " + tmp.getPath()); } catch (IOException e) { log.warn("Invalid temporary directory " + tmp.getPath() + ", using system default instead", e); tmp = null; } } getServletContext().setAttribute(ATTR_TMP_DIRECTORY, tmp); // force usage of custom locator factory. super.setLocatorFactory(new DavLocatorFactoryImpl(getResourcePathPrefix())); } protected String getResourcePathPrefix() { return getInitParameter(INIT_PARAM_RESOURCE_PATH_PREFIX); } @Override public DavResourceFactory getResourceFactory() { return new ResourceFactoryImpl(txMgr, subscriptionMgr); } @Override protected void doGet(WebdavRequest webdavRequest, WebdavResponse webdavResponse, DavResource davResource) throws IOException, DavException { if (canHandle(DavMethods.DAV_GET, webdavRequest, davResource)) { // return json representation of the requested resource DavResourceLocator locator = davResource.getLocator(); String path = locator.getRepositoryPath(); Session session = getRepositorySession(webdavRequest); try { Node node = session.getNode(path); int depth = ((WrappingLocator) locator).getDepth(); webdavResponse.setContentType("text/plain;charset=utf-8"); webdavResponse.setStatus(DavServletResponse.SC_OK); JsonWriter writer = new JsonWriter(webdavResponse.getWriter()); String[] includes = webdavRequest.getParameterValues(PARAM_INCLUDE); if (includes == null) { if (depth < BatchReadConfig.DEPTH_INFINITE) { NodeType type = node.getPrimaryNodeType(); depth = brConfig.getDepth(type.getName()); } writer.write(node, depth); } else { writeMultiple(writer, node, includes, depth); } } catch (PathNotFoundException e) { // properties cannot be requested as json object. throw new JcrDavException( new ItemNotFoundException("No node at " + path), DavServletResponse.SC_NOT_FOUND); } catch (RepositoryException e) { // should only get here if the item does not exist. log.debug(e.getMessage()); throw new JcrDavException(e); } } else { super.doGet(webdavRequest, webdavResponse, davResource); } } private void writeMultiple( JsonWriter writer, Node node, String[] includes, int depth) throws RepositoryException, IOException { Collection<Node> nodes = new ArrayList<Node>(); Set<String> alreadyAdded = new HashSet<String>(); for (String include : includes) { try { Node n; if (include.startsWith("/")) { n = node.getSession().getNode(include); } else { n = node.getNode(include); } String np = n.getPath(); if (!alreadyAdded.contains(np)) { nodes.add(n); alreadyAdded.add(np); } } catch (PathNotFoundException e) { // skip missing node } } writer.write(nodes, depth); } @Override protected void doPost(WebdavRequest webdavRequest, WebdavResponse webdavResponse, DavResource davResource) throws IOException, DavException { if (canHandle(DavMethods.DAV_POST, webdavRequest, davResource)) { // special remoting request: the defined parameters are exclusive // and cannot be combined. Session session = getRepositorySession(webdavRequest); RequestData data = new RequestData(webdavRequest, getTempDirectory(getServletContext())); String loc = null; try { String[] pValues; String[] includes = null; // multi-read over POST if ((pValues = data.getParameterValues(PARAM_CLONE)) != null) { loc = clone(session, pValues, davResource.getLocator()); } else if ((pValues = data.getParameterValues(PARAM_COPY)) != null) { loc = copy(session, pValues, davResource.getLocator()); } else if (data.getParameterValues(PARAM_DIFF) != null) { String targetPath = davResource.getLocator().getRepositoryPath(); processDiff(session, targetPath, data, protectedRemoveManager); } else if ((pValues = data.getParameterValues(PARAM_INCLUDE)) != null && canHandle(DavMethods.DAV_GET, webdavRequest, davResource)) { includes = pValues; } else { String targetPath = davResource.getLocator().getRepositoryPath(); loc = modifyContent(session, targetPath, data, protectedRemoveManager); } // TODO: append entity if (loc == null) { webdavResponse.setStatus(HttpServletResponse.SC_OK); if (includes != null) { webdavResponse.setContentType("text/plain;charset=utf-8"); JsonWriter writer = new JsonWriter(webdavResponse.getWriter()); DavResourceLocator locator = davResource.getLocator(); String path = locator.getRepositoryPath(); Node node = session.getNode(path); int depth = ((WrappingLocator) locator).getDepth(); writeMultiple(writer, node, includes, depth); } } else { webdavResponse.setHeader(DeltaVConstants.HEADER_LOCATION, loc); webdavResponse.setStatus(HttpServletResponse.SC_CREATED); } } catch (RepositoryException e) { log.warn(e.getMessage(), e); throw new JcrDavException(e); } catch (DiffException e) { log.warn(e.getMessage()); Throwable cause = e.getCause(); if (cause instanceof RepositoryException) { throw new JcrDavException((RepositoryException) cause); } else { throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Invalid diff format."); } } finally { data.dispose(); } } else { super.doPost(webdavRequest, webdavResponse, davResource); } } private boolean canHandle(int methodCode, WebdavRequest request, DavResource davResource) { DavResourceLocator locator = davResource.getLocator(); switch (methodCode) { case DavMethods.DAV_GET: return davResource.exists() && (locator instanceof WrappingLocator) && ((WrappingLocator) locator).isJsonRequest; case DavMethods.DAV_POST: String ct = request.getContentType(); if (ct == null) { return false; } else { int semicolon = ct.indexOf(';'); if (semicolon >= 0) { ct = ct.substring(0, semicolon); } ct = ct.trim().toLowerCase(Locale.ENGLISH); return "multipart/form-data".equals(ct) || "application/x-www-form-urlencoded".equals(ct); } default: return false; } } private static String clone(Session session, String[] cloneArgs, DavResourceLocator reqLocator) throws RepositoryException { Workspace wsp = session.getWorkspace(); String destPath = null; for (String cloneArg : cloneArgs) { String[] args = cloneArg.split(","); if (args.length == 4) { wsp.clone(args[0], args[1], args[2], Boolean.valueOf(args[3])); destPath = args[2]; } else { throw new RepositoryException(":clone parameter must have a value consisting of the 4 args needed for a Workspace.clone() call."); } } return buildLocationHref(session, destPath, reqLocator); } private static String copy(Session session, String[] copyArgs, DavResourceLocator reqLocator) throws RepositoryException { Workspace wsp = session.getWorkspace(); String destPath = null; for (String copyArg : copyArgs) { String[] args = copyArg.split(","); switch (args.length) { case 2: wsp.copy(args[0], args[1]); destPath = args[1]; break; case 3: wsp.copy(args[0], args[1], args[2]); destPath = args[2]; break; default: throw new RepositoryException(":copy parameter must have a value consisting of 2 jcr paths or workspaceName plus 2 jcr paths separated by ','."); } } return buildLocationHref(session, destPath, reqLocator); } private static String buildLocationHref(Session s, String destPath, DavResourceLocator reqLocator) throws RepositoryException { if (destPath != null) { NodeIterator it = s.getRootNode().getNodes(destPath.substring(1)); Node n = null; while (it.hasNext()) { n = it.nextNode(); } if (n != null) { DavResourceLocator loc = reqLocator.getFactory().createResourceLocator(reqLocator.getPrefix(), reqLocator.getWorkspacePath(), n.getPath(), false); return loc.getHref(true); } } // unable to determine -> no location header sent back. return null; } private static void processDiff(Session session, String targetPath, RequestData data, ProtectedRemoveManager protectedRemoveManager) throws RepositoryException, DiffException, IOException { String[] diffs = data.getParameterValues(PARAM_DIFF); DiffHandler handler = new JsonDiffHandler(session, targetPath, data, protectedRemoveManager); DiffParser parser = new DiffParser(handler); for (String diff : diffs) { boolean success = false; try { parser.parse(diff); session.save(); success = true; } finally { if (!success) { session.refresh(false); } } } } /** * TODO: doesn't work properly with intermediate SNS-nodes * TODO: doesn't respect jcr:uuid properties. * * @param session * @param targetPath * @param data * @throws RepositoryException * @throws DiffException */ private static String modifyContent(Session session, String targetPath, RequestData data, ProtectedRemoveManager protectedRemoveManager) throws RepositoryException, DiffException { JsonDiffHandler dh = new JsonDiffHandler(session, targetPath, data, protectedRemoveManager); boolean success = false; try { for (Iterator<String> pNames = data.getParameterNames(); pNames.hasNext();) { String paramName = pNames.next(); String propPath = dh.getItemPath(paramName); String parentPath = Text.getRelativeParent(propPath, 1); if (!session.itemExists(parentPath) || !session.getItem(parentPath).isNode()) { createNode(session, parentPath, data); } if (JcrConstants.JCR_PRIMARYTYPE.equals(Text.getName(propPath))) { // already handled by createNode above -> ignore continue; } // none of the special properties -> let the diffhandler take care // of the property creation/modification. dh.setProperty(paramName, null); } // save the complete set of modifications session.save(); success = true; } finally { if (!success) { session.refresh(false); } } return null; // TODO build loc-href if items were created. } /** * * @param session * @param nodePath * @param data * @throws RepositoryException */ private static void createNode(Session session, String nodePath, RequestData data) throws RepositoryException { Node parent = session.getRootNode(); String[] smgts = Text.explode(nodePath, '/'); for (String nodeName : smgts) { if (parent.hasNode(nodeName)) { parent = parent.getNode(nodeName); } else { // need to create the node // TODO: won't work for SNS String nPath = parent.getPath() + "/" + nodeName; String ntName = data.getParameter(nPath + "/" + JcrConstants.JCR_PRIMARYTYPE); if (ntName == null) { parent = parent.addNode(nodeName); } else { parent = parent.addNode(nodeName, ntName); } } } } /** * * @param request * @return * @throws DavException */ private static Session getRepositorySession(WebdavRequest request) throws DavException { DavSession ds = request.getDavSession(); return JcrDavSession.getRepositorySession(ds); } /** * Returns the temp directory * * @return the temp directory */ private static File getTempDirectory(ServletContext servletCtx) { return (File) servletCtx.getAttribute(ATTR_TMP_DIRECTORY); } //-------------------------------------------------------------------------- /** * Locator factory that specially deals with hrefs having a .json extension. */ private static class DavLocatorFactoryImpl extends org.apache.jackrabbit.webdav.jcr.DavLocatorFactoryImpl { public DavLocatorFactoryImpl(String s) { super(s); } @Override public DavResourceLocator createResourceLocator(String prefix, String href) { DavResourceLocator loc = super.createResourceLocator(prefix, href); if (endsWithJson(href)) { loc = new WrappingLocator(super.createResourceLocator(prefix, href)); } return loc; } @Override public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) { DavResourceLocator loc = super.createResourceLocator(prefix, workspacePath, path, isResourcePath); if (isResourcePath && endsWithJson(path)) { loc = new WrappingLocator(loc); } return loc; } private static boolean endsWithJson(String s) { return s.endsWith(".json"); } } /** * Resource locator that removes trailing .json extensions and depth * selector that do not form part of the repository path. * As the locator and it's factory do not have access to a JCR session * the <code>extraJson</code> flag may be reset later on. * * @see ResourceFactoryImpl#getItem(org.apache.jackrabbit.webdav.jcr.JcrDavSession, org.apache.jackrabbit.webdav.DavResourceLocator) */ private static class WrappingLocator implements DavResourceLocator { private final DavResourceLocator loc; private boolean isJsonRequest = true; private int depth = Integer.MIN_VALUE; private String repositoryPath; private WrappingLocator(DavResourceLocator loc) { this.loc = loc; } private void extract() { String rp = loc.getRepositoryPath(); rp = rp.substring(0, rp.lastIndexOf('.')); int pos = rp.lastIndexOf('.'); if (pos > -1) { String depthStr = rp.substring(pos + 1); try { depth = Integer.parseInt(depthStr); rp = rp.substring(0, pos); } catch (NumberFormatException e) { // apparently no depth-info -> ignore } } repositoryPath = rp; } private int getDepth() { if (isJsonRequest) { if (repositoryPath == null) { extract(); } return depth; } else { return Integer.MIN_VALUE; } } public String getPrefix() { return loc.getPrefix(); } public String getResourcePath() { return loc.getResourcePath(); } public String getWorkspacePath() { return loc.getWorkspacePath(); } public String getWorkspaceName() { return loc.getWorkspaceName(); } public boolean isSameWorkspace(DavResourceLocator davResourceLocator) { return loc.isSameWorkspace(davResourceLocator); } public boolean isSameWorkspace(String string) { return loc.isSameWorkspace(string); } public String getHref(boolean b) { return loc.getHref(b); } public boolean isRootLocation() { return loc.isRootLocation(); } public DavLocatorFactory getFactory() { return loc.getFactory(); } public String getRepositoryPath() { if (isJsonRequest) { if (repositoryPath == null) { extract(); } return repositoryPath; } else { return loc.getRepositoryPath(); } } } /** * Resource factory used to make sure that the .json extension was properly * interpreted. */ private static class ResourceFactoryImpl extends org.apache.jackrabbit.webdav.jcr.DavResourceFactoryImpl { /** * Create a new <code>DavResourceFactoryImpl</code>. * * @param txMgr * @param subsMgr */ public ResourceFactoryImpl(TxLockManagerImpl txMgr, SubscriptionManager subsMgr) { super(txMgr, subsMgr); } @Override protected Item getItem(JcrDavSession sessionImpl, DavResourceLocator locator) throws PathNotFoundException, RepositoryException { if (locator instanceof WrappingLocator && ((WrappingLocator)locator).isJsonRequest) { // check if the .json extension has been correctly interpreted. Session s = sessionImpl.getRepositorySession(); try { if (s.itemExists(((WrappingLocator)locator).loc.getRepositoryPath())) { // an item exists with the original calculated repo-path // -> assume that the repository item path ends with .json // or .depth.json. i.e. .json wasn't an extra extension // appended to request the json-serialization of the node. // -> change the flag in the WrappingLocator correspondingly. ((WrappingLocator) locator).isJsonRequest = false; } } catch (RepositoryException e) { // if the unmodified repository path isn't valid (e.g. /a/b[2].5.json) // -> ignore. } } return super.getItem(sessionImpl, locator); } } }