/* * Copyright (C) 2003-2012 eXo Platform SAS. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.exoplatform.wcm.connector.collaboration; import org.apache.commons.lang.StringUtils; import org.exoplatform.common.http.HTTPStatus; import org.exoplatform.ecm.utils.text.Text; import org.exoplatform.services.cms.CmsService; import org.exoplatform.services.cms.impl.Utils; import org.exoplatform.services.cms.link.NodeFinder; import org.exoplatform.services.cms.lock.LockService; import org.exoplatform.services.cms.relations.RelationsService; import org.exoplatform.services.jcr.ext.common.SessionProvider; import org.exoplatform.services.listener.ListenerService; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.rest.resource.ResourceContainer; import org.exoplatform.services.wcm.core.NodetypeConstant; import org.exoplatform.services.wcm.publication.WCMPublicationService; import org.exoplatform.services.wcm.utils.WCMCoreUtils; import javax.annotation.security.RolesAllowed; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.Session; import javax.jcr.lock.LockException; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * The RenameConnector aims to enhance the use of the _rename_ action on the Sites Explorer. * The system allows setting two values: _name_ and _title_. * The _title_ is a logical name that is used to display in the Sites Explorer. * The _name_ is the technical name of the file at JCR level. It is notably exposed via the WEBDAV layer. * * @LevelAPI Experimental * * @anchor RenameConnector */ @Path("/contents/rename/") public class RenameConnector implements ResourceContainer { private static final Log LOG = ExoLogger.getLogger(RenameConnector.class.getName()); private static final Pattern FILE_EXPLORER_URL_SYNTAX = Pattern.compile("([^:/]+):(/.*)"); private static final String RELATION_PROP = "exo:relation"; private static final String DEFAULT_NAME = "untitled"; /** * Gets _objectid_ of the renamed node. * Basically, _objectid_ is a pattern which is useful to find HTML tags of a specific node. * _objectid_ actually is the node path encoded by _URLEncoder_. * * @param nodePath The node path * @return _objectid_ * @throws Exception The exception * * @anchor RenameConnector.getObjectId */ @GET @Path("/getObjectId/") public Response getObjectId(@QueryParam("nodePath") String nodePath) throws Exception { return Response.ok(Utils.getObjectId(nodePath), MediaType.TEXT_PLAIN).build(); } /** * Calls RenameConnector REST service to execute the "_rename_" process. * * @param oldPath The old path of the renamed node with syntax: [workspace:node path] * @param newTitle The new title of the node. * @return Httpstatus 400 if renaming fails, otherwise the UUID of the renamed node is returned. * @throws Exception The exception * * @anchor RenameConnector.rename */ @GET @Path("/rename/") @RolesAllowed("users") public Response rename(@QueryParam("oldPath") String oldPath, @QueryParam("newTitle") String newTitle) throws Exception { try { // Check and escape newTitle if (StringUtils.isBlank(newTitle)) { return Response.status(HTTPStatus.BAD_REQUEST).build(); } String newExoTitle = Text.escapeIllegalJcrChars(newTitle); // Clarify new name & check to add extension String newName = Text.escapeIllegalJcrChars(org.exoplatform.services.cms.impl.Utils.cleanString(newTitle)); // Set default name if new title contain no valid character newName = (StringUtils.isEmpty(newName)) ? DEFAULT_NAME : newName; // Get renamed node String[] workspaceAndPath = parseWorkSpaceNameAndNodePath(oldPath); Node renamedNode = (Node)WCMCoreUtils.getService(NodeFinder.class) .getItem(this.getSession(workspaceAndPath[0]), workspaceAndPath[1], false); String oldName = renamedNode.getName(); if (oldName.indexOf('.') != -1 && renamedNode.isNodeType(NodetypeConstant.NT_FILE)) { String ext = oldName.substring(oldName.lastIndexOf('.')); newName = newName.concat(ext); newExoTitle = newExoTitle.concat(ext); } // Stop process if new name and exo:title is the same with old one String oldExoTitle = (renamedNode.hasProperty("exo:title")) ? renamedNode.getProperty("exo:title") .getString() : StringUtils.EMPTY; CmsService cmsService = WCMCoreUtils.getService(CmsService.class); cmsService.getPreProperties().clear(); String nodeUUID = ""; if(renamedNode.isNodeType(NodetypeConstant.MIX_REFERENCEABLE)) nodeUUID = renamedNode.getUUID(); cmsService.getPreProperties().put(nodeUUID + "_" + "exo:title", oldExoTitle); if (renamedNode.getName().equals(newName) && oldExoTitle.equals(newExoTitle)) { return Response.status(HTTPStatus.BAD_REQUEST).build(); } // Check if can edit locked node if (!this.canEditLockedNode(renamedNode)) { return Response.status(HTTPStatus.BAD_REQUEST).build(); } // Get uuid if (renamedNode.canAddMixin(NodetypeConstant.MIX_REFERENCEABLE)) { renamedNode.addMixin(NodetypeConstant.MIX_REFERENCEABLE); renamedNode.save(); } String uuid = renamedNode.getUUID(); // Only execute renaming if name is changed Session nodeSession = renamedNode.getSession(); if (!renamedNode.getName().equals(newName)) { // Backup relations pointing to the rename node List<Node> refList = new ArrayList<Node>(); PropertyIterator references = renamedNode.getReferences(); RelationsService relationsService = WCMCoreUtils.getService(RelationsService.class); while (references.hasNext()) { Property pro = references.nextProperty(); Node refNode = pro.getParent(); if (refNode.hasProperty(RELATION_PROP)) { refList.add(refNode); relationsService.removeRelation(refNode, renamedNode.getPath()); } } // Change name Node parent = renamedNode.getParent(); String srcPath = renamedNode.getPath(); String destPath = (parent.getPath().equals("/") ? StringUtils.EMPTY : parent.getPath()) + "/" + newName; this.addLockToken(renamedNode.getParent()); nodeSession.getWorkspace().move(srcPath, destPath); // Update renamed node Node destNode = nodeSession.getNodeByUUID(uuid); // Restore relation to new name node for (Node addRef : refList) { relationsService.addRelation(addRef, destNode.getPath(), nodeSession.getWorkspace().getName()); } // Update lock after moving if (destNode.isLocked()) { WCMCoreUtils.getService(LockService.class).changeLockToken(renamedNode, destNode); } this.changeLockForChild(srcPath, destNode); // Mark rename node as modified if (destNode.canAddMixin("exo:modify")) { destNode.addMixin("exo:modify"); } destNode.setProperty("exo:lastModifier", nodeSession.getUserID()); // Update exo:name if(renamedNode.canAddMixin("exo:sortable")) { renamedNode.addMixin("exo:sortable"); } renamedNode.setProperty("exo:name", renamedNode.getName()); renamedNode = destNode; } // Change title if (!renamedNode.hasProperty("exo:title")) { renamedNode.addMixin(NodetypeConstant.EXO_RSS_ENABLE); } renamedNode.setProperty("exo:title", newExoTitle); nodeSession.save(); // Update state of node WCMPublicationService publicationService = WCMCoreUtils.getService(WCMPublicationService.class); if (publicationService.isEnrolledInWCMLifecycle(renamedNode)) { ListenerService listenerService = WCMCoreUtils.getService(ListenerService.class); listenerService.broadcast(CmsService.POST_EDIT_CONTENT_EVENT, this, renamedNode); } return Response.ok(uuid).build(); } catch (LockException e) { if (LOG.isWarnEnabled()) { LOG.warn("The node or parent node is locked. Rename is not successful!"); } } catch (Exception e) { if (LOG.isDebugEnabled()) { LOG.debug("Rename is not successful!", e); } else if (LOG.isWarnEnabled()) { LOG.warn("Rename is not successful!"); } } return Response.status(HTTPStatus.BAD_REQUEST).build(); } /** * Updates lock for child nodes after renaming. * * @param srcPath The source path. * @param parentNewNode The destination node which gets the new name. * @throws Exception */ private void changeLockForChild(String srcPath, Node parentNewNode) throws Exception { if(parentNewNode.hasNodes()) { NodeIterator newNodeIter = parentNewNode.getNodes(); String newSRCPath = null; while(newNodeIter.hasNext()) { Node newChildNode = newNodeIter.nextNode(); newSRCPath = newChildNode.getPath().replace(parentNewNode.getPath(), srcPath); if(newChildNode.isLocked()) WCMCoreUtils.getService(LockService.class).changeLockToken(newSRCPath, newChildNode); if(newChildNode.hasNodes()) changeLockForChild(newSRCPath, newChildNode); } } } /** * Checks if a locked node is editable or not. * * @param node A specific node. * @return True if the locked node is editable, false otherwise. * @throws Exception */ private boolean canEditLockedNode(Node node) throws Exception { LockService lockService = WCMCoreUtils.getService(LockService.class); if(!node.isLocked()) return true; String lockToken = lockService.getLockTokenOfUser(node); if(lockToken != null) { node.getSession().addLockToken(lockService.getLockToken(node)); return true; } return false; } /** * Adds the lock token of a specific node to its session. * * @param node A specific node. * @throws Exception */ private void addLockToken(Node node) throws Exception { if (node.isLocked()) { String lockToken = WCMCoreUtils.getService(LockService.class).getLockToken(node); if(lockToken != null) { node.getSession().addLockToken(lockToken); } } } /** * Parse node path with syntax [workspace:node path] to workspace name and path separately * * @param nodePath node path with syntax [workspace:node path] * @return array of String. element with index 0 is workspace name, remaining one is node path */ private String[] parseWorkSpaceNameAndNodePath(String nodePath) { Matcher matcher = RenameConnector.FILE_EXPLORER_URL_SYNTAX.matcher(nodePath); if (!matcher.find()) return null; String[] workSpaceNameAndNodePath = new String[2]; workSpaceNameAndNodePath[0] = matcher.group(1); workSpaceNameAndNodePath[1] = matcher.group(2); return workSpaceNameAndNodePath; } /** * Gets user session from a specific workspace. * * @param workspaceName * @return session * @throws Exception */ private Session getSession(String workspaceName) throws Exception { SessionProvider sessionProvider = WCMCoreUtils.getUserSessionProvider(); return sessionProvider.getSession(workspaceName, WCMCoreUtils.getRepository()); } }