/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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.apereo.portal.layout.dlm; import java.util.Collections; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.xpath.XPathConstants; import org.apache.commons.lang.Validate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apereo.portal.IUserIdentityStore; import org.apereo.portal.IUserProfile; import org.apereo.portal.UserProfile; import org.apereo.portal.layout.IUserLayoutStore; import org.apereo.portal.security.provider.BrokenSecurityContext; import org.apereo.portal.security.provider.PersonImpl; import org.apereo.portal.utils.Tuple; import org.apereo.portal.xml.XmlUtilities; import org.apereo.portal.xml.xpath.XPathOperations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; /** * Factory object responsible for creating DLM reference objects like {@link Pathref}s and {@link * Noderef}s. One instance of NodeReferenceFactory should be present in the Spring context, whence * it also derives its dependencies. * */ @Component public final class NodeReferenceFactory { private static final Pattern DLM_PATH_REF_DELIM = Pattern.compile("\\:"); private static final Pattern USER_NODE_PATTERN = Pattern.compile("\\A([a-zA-Z]\\d*)\\z"); private static final Pattern DLM_NODE_PATTERN = Pattern.compile("u(\\d+)l\\d+([ns]\\d+)"); private final Log log = LogFactory.getLog(getClass()); @Autowired private IUserLayoutStore layoutStore; @Autowired private IUserIdentityStore userIdentityStore; @Autowired private XmlUtilities xmlUtilities; @Autowired private XPathOperations xPathOperations; /* * Public API. */ /** * Returns a valid {@link Noderef} based on the specified arguments or * <code>null</null> if that's not possible.<br/> * * <strong>This method returns <code>null</code> if the pathref cannot be * resolved to a node on the specified layout.</strong> It is the * responsibility of calling code to handle this case appropriately. * * @return a valid {@link Noderef} or <code>null</null> */ public Noderef getNoderefFromPathref( String layoutOwner, String pathref, String fname, boolean isStructRef, org.dom4j.Element layoutElement) { Validate.notNull(layoutOwner, "Argument 'layoutOwner' cannot be null."); Validate.notNull(pathref, "Argument 'pathref' cannot be null."); if (log.isTraceEnabled()) { StringBuilder msg = new StringBuilder(); msg.append("getDlmNoderef: [layoutOwner='") .append(layoutOwner) .append("', pathref='") .append(pathref) .append("', fname='") .append(fname) .append("', isStructRef='") .append(isStructRef) .append("']"); log.trace(msg.toString()); log.trace("getDlmNoderef: user layout document follows...\n" + layoutElement.asXML()); } final String[] pathTokens = DLM_PATH_REF_DELIM.split(pathref); if (pathTokens.length <= 1) { this.log.warn("Invalid DLM PathRef, no delimiter: " + pathref); return null; } if (pathTokens[0].equals(layoutOwner)) { // This an internal reference (our own layout); we have to // use the layoutExment (instead of load-limited-layout) b/c // our layout may not be in the db... final org.dom4j.Element target = (org.dom4j.Element) layoutElement.selectSingleNode(pathTokens[1]); if (target != null) { return new Noderef(target.valueOf("@ID")); } this.log.warn( "Unable to resolve pathref '" + pathref + "' for layoutOwner '" + layoutOwner + "'"); return null; } /* * We know this Noderef refers to a node on a DLM fragment */ final String layoutOwnerName = pathTokens[0]; final String layoutPath = pathTokens[1]; final Integer layoutOwnerUserId = this.userIdentityStore.getPortalUserId(layoutOwnerName); if (layoutOwnerUserId == null) { this.log.warn( "Unable to resolve pathref '" + pathref + "' for layoutOwner '" + layoutOwner + "', no userId found for userName: " + layoutOwnerName); return null; } final Tuple<String, DistributedUserLayout> userLayoutInfo = getUserLayoutTuple(layoutOwnerName, layoutOwnerUserId); final Document userLayout = userLayoutInfo.second.getLayout(); final Node targetNode = this.xPathOperations.evaluate(layoutPath, userLayout, XPathConstants.NODE); if (targetNode == null) { this.log.warn("No layout node found for pathref: " + pathref); return null; } final NamedNodeMap attributes = targetNode.getAttributes(); if (fname != null) { final Node fnameAttr = attributes.getNamedItem("fname"); if (fnameAttr == null) { this.log.warn("Layout node for pathref does not have fname attribute: " + pathref); return null; } final String nodeFname = fnameAttr.getTextContent(); if (!fname.equals(nodeFname)) { this.log.warn( "fname '" + nodeFname + "' on layout node not match specified fname '" + fname + "' for pathref: " + pathref); return null; } } final Node structIdAttr = attributes.getNamedItem("struct-id"); if (structIdAttr != null) { final String structId = structIdAttr.getTextContent(); if (isStructRef) { return new Noderef( layoutOwnerUserId, 1 /* TODO: remove hard-coded layoutId=1 */, "s" + structId); } return new Noderef( layoutOwnerUserId, 1 /* TODO: remove hard-coded layoutId=1 */, "n" + structId); } final Node idAttr = attributes.getNamedItem("ID"); return new Noderef( layoutOwnerUserId, 1 /* TODO: remove hard-coded layoutId=1 */, idAttr.getTextContent()); } /** * Returns a valid {@link Pathref} based on the specified arguments or * <code>null</null> if that's not possible. * * @param layoutOwnerUsername * @param dlmNoderef * @param layout * @return a valid {@link Pathref} or <code>null</null> */ public Pathref getPathrefFromNoderef( String layoutOwnerUsername, String dlmNoderef, org.dom4j.Element layout) { Validate.notNull(layoutOwnerUsername, "Argument 'layoutOwnerUsername' cannot be null."); Validate.notNull(dlmNoderef, "Argument 'dlmNoderef' cannot be null."); if (log.isTraceEnabled()) { StringBuilder msg = new StringBuilder(); msg.append("createPathref: [layoutOwnerUsername='") .append(layoutOwnerUsername) .append("', dlmNoderef='") .append(dlmNoderef) .append("']"); log.trace(msg.toString()); } Pathref rslt = null; // default; signifies we can't match a node final Matcher dlmNodeMatcher = DLM_NODE_PATTERN.matcher(dlmNoderef); if (dlmNodeMatcher.matches()) { final int userId = Integer.valueOf(dlmNodeMatcher.group(1)); final String nodeId = dlmNodeMatcher.group(2); final String userName = this.userIdentityStore.getPortalUserName(userId); final Tuple<String, DistributedUserLayout> userLayoutInfo = getUserLayoutTuple(userName, userId); if (userLayoutInfo.second == null) { this.log.warn( "no layout for fragment user '" + userLayoutInfo.first + "' Specified dlmNoderef " + dlmNoderef + " cannot be resolved."); return null; } final Document fragmentLayout = userLayoutInfo.second.getLayout(); final Node targetElement = this.xPathOperations.evaluate( "//*[@ID = $nodeId]", Collections.singletonMap("nodeId", nodeId), fragmentLayout, XPathConstants.NODE); // We can only proceed if there's a valid match in the document if (targetElement != null) { String xpath = this.xmlUtilities.getUniqueXPath(targetElement); // Pathref objects that refer to portlets are expected to include // the fname as the 3rd element; other pathref objects should leave // that element blank. String fname = null; Node fnameAttr = targetElement.getAttributes().getNamedItem("fname"); if (fnameAttr != null) { fname = fnameAttr.getTextContent(); } rslt = new Pathref(userLayoutInfo.first, xpath, fname); } } final Matcher userNodeMatcher = USER_NODE_PATTERN.matcher(dlmNoderef); if (userNodeMatcher.find()) { // We need a pathref based on the new style of layout b/c on // import this users own layout will not be in the database // when the path is computed back to an Id... final String structId = userNodeMatcher.group(1); final org.dom4j.Node target = layout.selectSingleNode("//*[@ID = '" + structId + "']"); if (target == null) { this.log.warn( "no match found on layout for user '" + layoutOwnerUsername + "' for the specified dlmNoderef: " + dlmNoderef); return null; } String fname = null; if (target.getName().equals("channel")) { fname = target.valueOf("@fname"); } rslt = new Pathref(layoutOwnerUsername, target.getUniquePath(), fname); } return rslt; } /* * Implementation. */ /** * Provides a {@link Tuple} containing the "fragmentized" version of a DLM fragment * owner's layout, together with the username. This version of the layout consistent with what * DLM uses internally for fragments, and is created by FragmentActivator.fragmentizeLayout. * It's important that the version returned by this method matches what DLM uses internally * because it will be used to establish relationships between fragment layout nodes and user * customizations of DLM fragments. * * @param userId * @return */ /* TODO: make private */ Tuple<String, DistributedUserLayout> getUserLayoutTuple( String userName, int userId) { final PersonImpl person = new PersonImpl(); person.setUserName(userName); person.setID(userId); person.setSecurityContext(new BrokenSecurityContext()); final IUserProfile profile = layoutStore.getUserProfileByFname(person, UserProfile.DEFAULT_PROFILE_FNAME); final DistributedUserLayout userLayout = layoutStore.getUserLayout(person, (UserProfile) profile); return new Tuple<String, DistributedUserLayout>(userName, userLayout); } }