View Javadoc
1   /*
2    * Copyright (C) 2003-2012 eXo Platform SAS.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program. If not, see <http://www.gnu.org/licenses/>.
16   */
17  package org.exoplatform.wcm.connector.collaboration;
18  
19  import org.apache.commons.lang.StringUtils;
20  import org.exoplatform.common.http.HTTPStatus;
21  import org.exoplatform.ecm.utils.text.Text;
22  import org.exoplatform.services.cms.CmsService;
23  import org.exoplatform.services.cms.impl.Utils;
24  import org.exoplatform.services.cms.link.NodeFinder;
25  import org.exoplatform.services.cms.lock.LockService;
26  import org.exoplatform.services.cms.relations.RelationsService;
27  import org.exoplatform.services.jcr.ext.common.SessionProvider;
28  import org.exoplatform.services.listener.ListenerService;
29  import org.exoplatform.services.log.ExoLogger;
30  import org.exoplatform.services.log.Log;
31  import org.exoplatform.services.rest.resource.ResourceContainer;
32  import org.exoplatform.services.wcm.core.NodetypeConstant;
33  import org.exoplatform.services.wcm.publication.WCMPublicationService;
34  import org.exoplatform.services.wcm.utils.WCMCoreUtils;
35  
36  import javax.annotation.security.RolesAllowed;
37  import javax.jcr.Node;
38  import javax.jcr.NodeIterator;
39  import javax.jcr.Property;
40  import javax.jcr.PropertyIterator;
41  import javax.jcr.Session;
42  import javax.jcr.lock.LockException;
43  import javax.ws.rs.GET;
44  import javax.ws.rs.Path;
45  import javax.ws.rs.QueryParam;
46  import javax.ws.rs.core.MediaType;
47  import javax.ws.rs.core.Response;
48  import java.util.ArrayList;
49  import java.util.List;
50  import java.util.regex.Matcher;
51  import java.util.regex.Pattern;
52  
53  /**
54   * The RenameConnector aims to enhance the use of the _rename_ action on the Sites Explorer.
55   * The system allows setting two values: _name_ and _title_.
56   * The _title_ is a logical name that is used to display in the Sites Explorer.
57   * The _name_ is the technical name of the file at JCR level. It is notably exposed via the WEBDAV layer.
58   *
59   * @LevelAPI Experimental
60   *
61   * @anchor RenameConnector
62   */
63  @Path("/contents/rename/")
64  public class RenameConnector implements ResourceContainer {
65  
66    private static final Log     LOG                      = ExoLogger.getLogger(RenameConnector.class.getName());
67  
68    private static final Pattern FILE_EXPLORER_URL_SYNTAX = Pattern.compile("([^:/]+):(/.*)");
69  
70    private static final String  RELATION_PROP            = "exo:relation";
71    
72    private static final String DEFAULT_NAME = "untitled";
73  
74    /**
75     * Gets _objectid_ of the renamed node.
76     * Basically, _objectid_ is a pattern which is useful to find HTML tags of a specific node.
77     * _objectid_ actually is the node path encoded by _URLEncoder_.
78     *
79     * @param nodePath The node path
80     * @return _objectid_
81     * @throws Exception The exception
82     *
83     * @anchor RenameConnector.getObjectId
84     */
85    @GET
86    @Path("/getObjectId/")
87    public Response getObjectId(@QueryParam("nodePath") String nodePath) throws Exception {
88      return Response.ok(Utils.getObjectId(nodePath), MediaType.TEXT_PLAIN).build();
89    }
90  
91    /**
92     * Calls RenameConnector REST service to execute the "_rename_" process.
93     *
94     * @param oldPath The old path of the renamed node with syntax: [workspace:node path]
95     * @param newTitle The new title of the node.
96     * @return Httpstatus 400 if renaming fails, otherwise the UUID of the renamed node is returned.
97     * @throws Exception The exception
98     *
99     * @anchor RenameConnector.rename
100    */
101   @GET
102   @Path("/rename/")
103   @RolesAllowed("users")
104   public Response rename(@QueryParam("oldPath") String oldPath,
105                          @QueryParam("newTitle") String newTitle) throws Exception {
106     try {
107       // Check and escape newTitle
108       if (StringUtils.isBlank(newTitle)) {
109         return Response.status(HTTPStatus.BAD_REQUEST).build();
110       }
111       String newExoTitle = newTitle;
112       // Clarify new name & check to add extension
113       String newName = Text.escapeIllegalJcrChars(newTitle);
114       
115       // Set default name if new title contain no valid character
116       newName = (StringUtils.isEmpty(newName)) ? DEFAULT_NAME : newName;
117 
118       // Get renamed node
119       String[] workspaceAndPath = parseWorkSpaceNameAndNodePath(oldPath);
120       Node renamedNode = (Node)WCMCoreUtils.getService(NodeFinder.class)
121               .getItem(this.getSession(workspaceAndPath[0]), workspaceAndPath[1], false);
122 
123       String oldName = renamedNode.getName();
124       if (oldName.indexOf('.') != -1 && renamedNode.isNodeType(NodetypeConstant.NT_FILE)) {
125         String ext = oldName.substring(oldName.lastIndexOf('.'));
126         newName = newName.concat(ext);
127         newExoTitle = newExoTitle.concat(ext);
128       }
129 
130       // Stop process if new name and exo:title is the same with old one
131       String oldExoTitle = (renamedNode.hasProperty("exo:title")) ? renamedNode.getProperty("exo:title")
132                                                                                .getString()
133                                                                  : StringUtils.EMPTY;
134       CmsService cmsService = WCMCoreUtils.getService(CmsService.class);
135       cmsService.getPreProperties().clear();
136       String nodeUUID = "";
137       if(renamedNode.isNodeType(NodetypeConstant.MIX_REFERENCEABLE)) nodeUUID = renamedNode.getUUID();
138       cmsService.getPreProperties().put(nodeUUID + "_" + "exo:title", oldExoTitle);
139       
140       if (renamedNode.getName().equals(newName) && oldExoTitle.equals(newExoTitle)) {
141         return Response.status(HTTPStatus.BAD_REQUEST).build();
142       }
143 
144       // Check if can edit locked node
145       if (!this.canEditLockedNode(renamedNode)) {
146         return Response.status(HTTPStatus.BAD_REQUEST).build();
147       }
148 
149       // Get uuid
150       if (renamedNode.canAddMixin(NodetypeConstant.MIX_REFERENCEABLE)) {
151         renamedNode.addMixin(NodetypeConstant.MIX_REFERENCEABLE);
152         renamedNode.save();
153       }
154       String uuid = renamedNode.getUUID();
155 
156       // Only execute renaming if name is changed
157       Session nodeSession = renamedNode.getSession();
158       if (!renamedNode.getName().equals(newName)) {
159         // Backup relations pointing to the rename node
160         List<Node> refList = new ArrayList<Node>();
161         PropertyIterator references = renamedNode.getReferences();
162         RelationsService relationsService = WCMCoreUtils.getService(RelationsService.class);
163         while (references.hasNext()) {
164           Property pro = references.nextProperty();
165           Node refNode = pro.getParent();
166           if (refNode.hasProperty(RELATION_PROP)) {
167             refList.add(refNode);
168             relationsService.removeRelation(refNode, renamedNode.getPath());
169           }
170         }
171 
172         // Change name
173         Node parent = renamedNode.getParent();
174         String srcPath = renamedNode.getPath();
175         String destPath = (parent.getPath().equals("/") ? StringUtils.EMPTY : parent.getPath()) + "/"
176             + newName;
177 
178         this.addLockToken(renamedNode.getParent());
179         nodeSession.getWorkspace().move(srcPath, destPath);
180 
181         // Update renamed node
182         Node destNode = nodeSession.getNodeByUUID(uuid);
183 
184         // Restore relation to new name node
185         for (Node addRef : refList) {
186           relationsService.addRelation(addRef, destNode.getPath(), nodeSession.getWorkspace().getName());
187         }
188 
189         // Update lock after moving
190         if (destNode.isLocked()) {
191           WCMCoreUtils.getService(LockService.class).changeLockToken(renamedNode, destNode);
192         }
193         this.changeLockForChild(srcPath, destNode);
194 
195         // Mark rename node as modified
196         if (destNode.canAddMixin("exo:modify")) {
197           destNode.addMixin("exo:modify");
198         }
199         destNode.setProperty("exo:lastModifier", nodeSession.getUserID());
200         
201         // Update exo:name
202         if(renamedNode.canAddMixin("exo:sortable")) {
203           renamedNode.addMixin("exo:sortable");
204         }
205         renamedNode.setProperty("exo:name", renamedNode.getName());
206 
207         renamedNode = destNode;
208       }
209 
210       // Change title
211       if (!renamedNode.hasProperty("exo:title")) {
212         renamedNode.addMixin(NodetypeConstant.EXO_RSS_ENABLE);
213       }
214       renamedNode.setProperty("exo:title", newExoTitle);
215 
216       nodeSession.save();
217       
218       // Update state of node
219       WCMPublicationService publicationService = WCMCoreUtils.getService(WCMPublicationService.class);
220       if (publicationService.isEnrolledInWCMLifecycle(renamedNode)) {
221         ListenerService listenerService = WCMCoreUtils.getService(ListenerService.class);
222         listenerService.broadcast(CmsService.POST_EDIT_CONTENT_EVENT, this, renamedNode);
223       }
224 
225       return Response.ok(uuid).build();
226     } catch (LockException e) {
227       if (LOG.isWarnEnabled()) {
228         LOG.warn("The node or parent node is locked. Rename is not successful!");
229       }
230     } catch (Exception e) {
231       if (LOG.isDebugEnabled()) {
232         LOG.debug("Rename is not successful!", e);
233       } else if (LOG.isWarnEnabled()) {
234         LOG.warn("Rename is not successful!");
235       }
236     }
237     return Response.status(HTTPStatus.BAD_REQUEST).build();
238   }
239 
240   /**
241    * Updates lock for child nodes after renaming.
242    *
243    * @param srcPath The source path.
244    * @param parentNewNode The destination node which gets the new name.
245    * @throws Exception
246    */
247   private void changeLockForChild(String srcPath, Node parentNewNode) throws Exception {
248     if(parentNewNode.hasNodes()) {
249       NodeIterator newNodeIter = parentNewNode.getNodes();
250       String newSRCPath = null;
251       while(newNodeIter.hasNext()) {
252         Node newChildNode = newNodeIter.nextNode();
253         newSRCPath = newChildNode.getPath().replace(parentNewNode.getPath(), srcPath);
254         if(newChildNode.isLocked()) WCMCoreUtils.getService(LockService.class).changeLockToken(newSRCPath, newChildNode);
255         if(newChildNode.hasNodes()) changeLockForChild(newSRCPath, newChildNode);
256       }
257     }
258   }
259 
260   /**
261    * Checks if a locked node is editable or not.
262    *
263    * @param node A specific node.
264    * @return True if the locked node is editable, false otherwise.
265    * @throws Exception
266    */
267   private boolean canEditLockedNode(Node node) throws Exception {
268     LockService lockService = WCMCoreUtils.getService(LockService.class);
269     if(!node.isLocked()) return true;
270     String lockToken = lockService.getLockTokenOfUser(node);
271     if(lockToken != null) {
272       node.getSession().addLockToken(lockService.getLockToken(node));
273       return true;
274     }
275     return false;
276   }
277 
278   /**
279    * Adds the lock token of a specific node to its session.
280    *
281    * @param node A specific node.
282    * @throws Exception
283    */
284   private void addLockToken(Node node) throws Exception {
285     if (node.isLocked()) {
286       String lockToken = WCMCoreUtils.getService(LockService.class).getLockToken(node);
287       if(lockToken != null) {
288         node.getSession().addLockToken(lockToken);
289       }
290     }
291   }
292 
293   /**
294    * Parse node path with syntax [workspace:node path] to workspace name and path separately
295    *
296    * @param nodePath node path with syntax [workspace:node path]
297    * @return array of String. element with index 0 is workspace name, remaining one is node path
298    */
299   private String[] parseWorkSpaceNameAndNodePath(String nodePath) {
300     Matcher matcher = RenameConnector.FILE_EXPLORER_URL_SYNTAX.matcher(nodePath);
301     if (!matcher.find())
302       return null;
303     String[] workSpaceNameAndNodePath = new String[2];
304     workSpaceNameAndNodePath[0] = matcher.group(1);
305     workSpaceNameAndNodePath[1] = matcher.group(2);
306     return workSpaceNameAndNodePath;
307   }
308 
309   /**
310    * Gets user session from a specific workspace.
311    *
312    * @param workspaceName
313    * @return session
314    * @throws Exception
315    */
316   private Session getSession(String workspaceName) throws Exception {
317     SessionProvider sessionProvider = WCMCoreUtils.getUserSessionProvider();
318     return sessionProvider.getSession(workspaceName, WCMCoreUtils.getRepository());
319   }
320 }