1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.module.exchangesimple;
35
36 import info.magnolia.cms.beans.runtime.Document;
37 import info.magnolia.cms.beans.runtime.MultipartForm;
38 import info.magnolia.cms.core.Access;
39 import info.magnolia.cms.core.Content;
40 import info.magnolia.cms.core.HierarchyManager;
41 import info.magnolia.cms.core.ItemType;
42 import info.magnolia.cms.core.NodeData;
43 import info.magnolia.cms.core.SystemProperty;
44 import info.magnolia.cms.exchange.ExchangeException;
45 import info.magnolia.cms.filters.AbstractMgnlFilter;
46 import info.magnolia.cms.security.AccessDeniedException;
47 import info.magnolia.cms.security.Permission;
48 import info.magnolia.cms.util.ContentUtil;
49 import info.magnolia.cms.util.Rule;
50 import info.magnolia.cms.util.RuleBasedContentFilter;
51 import info.magnolia.context.MgnlContext;
52
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.security.InvalidParameterException;
56 import java.util.Iterator;
57 import java.util.List;
58 import java.util.zip.GZIPInputStream;
59
60 import javax.jcr.ImportUUIDBehavior;
61 import javax.jcr.ItemNotFoundException;
62 import javax.jcr.Node;
63 import javax.jcr.PathNotFoundException;
64 import javax.jcr.Property;
65 import javax.jcr.PropertyType;
66 import javax.jcr.RepositoryException;
67 import javax.jcr.UnsupportedRepositoryOperationException;
68 import javax.jcr.lock.LockException;
69 import javax.servlet.FilterChain;
70 import javax.servlet.ServletException;
71 import javax.servlet.http.HttpServletRequest;
72 import javax.servlet.http.HttpServletResponse;
73 import javax.servlet.http.HttpSession;
74
75 import org.apache.commons.codec.binary.Base64;
76 import org.apache.commons.io.IOUtils;
77 import org.apache.commons.lang.StringUtils;
78 import org.jdom.Element;
79 import org.jdom.JDOMException;
80 import org.jdom.input.SAXBuilder;
81 import org.safehaus.uuid.UUIDGenerator;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
84
85
86
87
88
89
90
91 public class ReceiveFilter extends AbstractMgnlFilter {
92
93 private static final Logger log = LoggerFactory.getLogger(ReceiveFilter.class);
94
95
96
97
98 @Deprecated
99 private static final String SIBLING_UUID_3_0 = "UUID";
100
101 private int unlockRetries = 10;
102
103 private int retryWait = 2;
104
105 public int getUnlockRetries() {
106 return unlockRetries;
107 }
108
109 public void setUnlockRetries(int unlockRetries) {
110 this.unlockRetries = unlockRetries;
111 }
112
113 public long getRetryWait() {
114 return retryWait;
115 }
116
117 public void setRetryWait(int retryWait) {
118 this.retryWait = retryWait;
119 }
120
121 @Override
122 public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
123 String statusMessage = "";
124 String status = "";
125 String result = null;
126 try {
127 final String utf8AuthorStatus = request.getHeader(BaseSyndicatorImpl.UTF8_STATUS);
128
129 if (utf8AuthorStatus != null && (Boolean.parseBoolean(utf8AuthorStatus) != SystemProperty.getBooleanProperty(SystemProperty.MAGNOLIA_UTF8_ENABLED))) {
130 throw new UnsupportedOperationException("Activation between instances with different UTF-8 setting is not supported.");
131 }
132 final String action = request.getHeader(BaseSyndicatorImpl.ACTION);
133 if (action == null) {
134 throw new InvalidParameterException("Activation action must be set for each activation request.");
135 }
136
137 applyLock(request);
138 result = receive(request);
139 status = BaseSyndicatorImpl.ACTIVATION_SUCCESSFUL;
140 }
141 catch (OutOfMemoryError e) {
142 Runtime rt = Runtime.getRuntime();
143 log.error("---------\nOutOfMemoryError caught during activation. Total memory = "
144 + rt.totalMemory()
145 + ", free memory = "
146 + rt.freeMemory()
147 + "\n---------");
148 statusMessage = e.getMessage();
149 status = BaseSyndicatorImpl.ACTIVATION_FAILED;
150 }
151 catch (PathNotFoundException e) {
152
153 log.error(e.getMessage(), e);
154 statusMessage = "Parent not found (not yet activated): " + e.getMessage();
155 status = BaseSyndicatorImpl.ACTIVATION_FAILED;
156 } catch (ExchangeException e) {
157 log.debug(e.getMessage(), e);
158 statusMessage = e.getMessage();
159 status = BaseSyndicatorImpl.ACTIVATION_FAILED;
160 } catch (Throwable e) {
161 log.error(e.getMessage(), e);
162
163 statusMessage = StringUtils.defaultIfEmpty(e.getMessage(), e.getClass().getSimpleName());
164 status = BaseSyndicatorImpl.ACTIVATION_FAILED;
165 }
166 finally {
167 cleanUp(request);
168 setResponseHeaders(response, statusMessage, status, result);
169 }
170 }
171
172 protected void setResponseHeaders(HttpServletResponse response, String statusMessage, String status, String result) {
173 response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_STATUS, status);
174 response.setHeader(BaseSyndicatorImpl.ACTIVATION_ATTRIBUTE_MESSAGE, statusMessage);
175 }
176
177
178
179
180
181
182 protected synchronized String receive(HttpServletRequest request) throws Exception {
183 String action = request.getHeader(BaseSyndicatorImpl.ACTION);
184 log.debug("action: " + action);
185 String authorization = getUser(request);
186 String webapp = getWebappName();
187
188 if (action.equalsIgnoreCase(BaseSyndicatorImpl.ACTIVATE)) {
189 String name = update(request);
190
191 log.info("User {} successfuly activated {} on {}.", new Object[]{authorization, name, webapp});
192 }
193 else if (action.equalsIgnoreCase(BaseSyndicatorImpl.DEACTIVATE)) {
194 String name = remove(request);
195
196 log.info("User {} succeessfuly deactivated {} on {}.", new Object[] {authorization, name, webapp});
197 }
198 else {
199 throw new UnsupportedOperationException("Method not supported : " + action);
200 }
201 return null;
202 }
203
204 protected String getWebappName() {
205 return SystemProperty.getProperty(SystemProperty.MAGNOLIA_WEBAPP);
206 }
207
208 protected String getUser(HttpServletRequest request) {
209
210 String user = request.getHeader(BaseSyndicatorImpl.AUTHORIZATION);
211 if (StringUtils.isEmpty(user)) {
212 user = request.getParameter(BaseSyndicatorImpl.AUTH_USER);
213 } else {
214 user = new String(Base64.decodeBase64(user.substring(6).getBytes()));
215 user = user.substring(0, user.indexOf(":"));
216 }
217 return user;
218 }
219
220
221
222
223
224
225 protected synchronized String update(HttpServletRequest request) throws Exception {
226 MultipartForm data = MgnlContext.getPostedForm();
227 if (null != data) {
228 String newParentPath = this.getParentPath(request);
229 String resourceFileName = request.getHeader(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE);
230 HierarchyManager hm = getHierarchyManager(request);
231 Element rootElement = getImportedContentRoot(data, resourceFileName);
232 Element topContentElement = rootElement.getChild(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT);
233 Content content = null;
234 try {
235 String uuid = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE);
236 content = hm.getContentByUUID(uuid);
237
238 newParentPath = handleMovedContent(newParentPath, hm, topContentElement, content);
239 handleChildren(request, content);
240 this.importOnExisting(topContentElement, data, hm, content);
241 }
242 catch (ItemNotFoundException e) {
243
244 importFresh(topContentElement, data, hm, newParentPath);
245 }
246
247 return orderImportedNode(newParentPath, hm, rootElement, topContentElement);
248 }
249 return null;
250 }
251
252 protected Element getImportedContentRoot(MultipartForm data, String resourceFileName) throws JDOMException, IOException {
253 Document resourceDocument = data.getDocument(resourceFileName);
254 SAXBuilder builder = new SAXBuilder();
255 InputStream documentInputStream = resourceDocument.getStream();
256 org.jdom.Document jdomDocument = builder.build(documentInputStream);
257 IOUtils.closeQuietly(documentInputStream);
258 return jdomDocument.getRootElement();
259 }
260
261 protected void handleChildren(HttpServletRequest request, Content content) {
262 String ruleString = request.getHeader(BaseSyndicatorImpl.CONTENT_FILTER_RULE);
263 Rule rule = new Rule(ruleString, ",");
264 RuleBasedContentFilter filter = new RuleBasedContentFilter(rule);
265
266 this.removeChildren(content, filter);
267 }
268
269 protected String handleMovedContent(String newParentPath,
270 HierarchyManager hm, Element topContentElement, Content content)
271 throws RepositoryException {
272 String currentParentPath = content.getHandle();
273 currentParentPath = currentParentPath.substring(0, currentParentPath.lastIndexOf('/'));
274 String newName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
275 if (!newParentPath.endsWith("/")) {
276 newParentPath += "/";
277 }
278 if (!currentParentPath.endsWith("/")) {
279 currentParentPath += "/";
280 }
281 if (!newParentPath.equals(currentParentPath) || !content.getName().equals(newName)) {
282 log.info("Moving content from {} to {} due to activation request.", new Object[] { content.getHandle(), newParentPath + newName});
283 hm.moveTo(content.getHandle(), newParentPath + newName);
284 }
285 return newParentPath;
286 }
287
288 protected String orderImportedNode(String newParentPath, HierarchyManager hm, Element rootElement, Element topContentElement) throws RepositoryException {
289 String name;
290
291 name = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
292 Content parent = hm.getContent(newParentPath);
293 List siblings = rootElement.getChild(BaseSyndicatorImpl.SIBLINGS_ROOT_ELEMENT).getChildren(BaseSyndicatorImpl.SIBLINGS_ELEMENT);
294 Iterator siblingsIterator = siblings.iterator();
295 while (siblingsIterator.hasNext()) {
296 Element sibling = (Element) siblingsIterator.next();
297
298 try {
299 String siblingUUID = sibling.getAttributeValue(BaseSyndicatorImpl.SIBLING_UUID);
300
301 if (StringUtils.isEmpty(siblingUUID)) {
302 log.debug("Activating from a Magnolia 3.0 instance");
303 siblingUUID = sibling.getAttributeValue(SIBLING_UUID_3_0);
304 }
305 Content beforeContent = hm.getContentByUUID(siblingUUID);
306 log.debug("Ordering {} before {}", name, beforeContent.getName());
307 order(parent, name, beforeContent.getName());
308 break;
309 } catch (ItemNotFoundException e) {
310
311 } catch (RepositoryException re) {
312 log.warn("Failed to order node");
313 log.debug("Failed to order node", re);
314 }
315 }
316
317
318 if (siblings.isEmpty()) {
319 order(parent, name, null);
320 }
321 return name;
322 }
323
324
325 protected void order(Content parent, String name, String orderBefore) throws RepositoryException {
326 try {
327 parent.orderBefore(name, orderBefore);
328 } catch (UnsupportedRepositoryOperationException e) {
329
330 log.warn("Failed to order unorderable content {} at {} due to {}", new Object[] {name, parent.getHandle(), e.getMessage()});
331 }
332 parent.save();
333 }
334
335
336
337
338
339
340 protected synchronized void copyProperties(Content source, Content destination) throws RepositoryException {
341
342
343 Iterator nodeDataIterator = destination.getNodeDataCollection().iterator();
344 while (nodeDataIterator.hasNext()) {
345 NodeData nodeData = (NodeData) nodeDataIterator.next();
346
347
348 if (nodeData.getType() != PropertyType.BINARY) {
349 nodeData.delete();
350 }
351 }
352
353
354 Node destinationNode = destination.getJCRNode();
355 nodeDataIterator = source.getNodeDataCollection().iterator();
356 while (nodeDataIterator.hasNext()) {
357 NodeData nodeData = (NodeData) nodeDataIterator.next();
358 Property property = nodeData.getJCRProperty();
359 if (property.getDefinition().isMultiple()) {
360 if (destination.isGranted(Permission.WRITE)) {
361 destinationNode.setProperty(nodeData.getName(), property.getValues());
362 }
363 else {
364 throw new AccessDeniedException("User not allowed to " + Permission.PERMISSION_NAME_WRITE + " at [" + nodeData.getHandle() + "]");
365 }
366 }
367 else {
368 destination.createNodeData(nodeData.getName(), nodeData.getValue());
369 }
370 }
371 }
372
373
374
375
376
377
378 protected synchronized void removeChildren(Content content, Content.ContentFilter filter) {
379 Iterator children = content.getChildren(filter).iterator();
380
381
382 while (children.hasNext()) {
383 Content child = (Content) children.next();
384 try {
385 child.delete();
386 }
387 catch (Exception e) {
388 log.error("Failed to remove " + child.getHandle() + " | " + e.getMessage());
389 }
390 }
391 }
392
393
394
395
396
397
398
399
400
401
402 protected synchronized void importFresh(Element topContentElement, MultipartForm data, HierarchyManager hierarchyManager, String parentPath) throws ExchangeException, RepositoryException {
403
404
405 String path = parentPath + (parentPath.endsWith("/") ? "" : "/") + topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
406 if (hierarchyManager.isExist(path)) {
407 log.warn("Replacing {} due to name collision (but different UUIDs.)", path);
408 hierarchyManager.delete(path);
409 }
410 try {
411 importResource(data, topContentElement, hierarchyManager, parentPath);
412 hierarchyManager.save();
413 } catch (PathNotFoundException e) {
414 final String message = "Parent content " + parentPath + " is not yet activated or you do not have write access to it. Please activate the parent content before activating children and ensure you have appropriate rights";
415
416 log.debug(message, e);
417 hierarchyManager.refresh(false);
418 throw new ExchangeException(message);
419 } catch (Exception e) {
420 final String message = "Activation failed | " + e.getMessage();
421 log.error("Exception caught", e);
422 hierarchyManager.refresh(false);
423 throw new ExchangeException(message);
424 }
425 }
426
427
428
429
430
431
432
433
434
435
436 protected synchronized void importOnExisting(Element topContentElement, MultipartForm data,
437 final HierarchyManager hierarchyManager, Content existingContent) throws ExchangeException, RepositoryException {
438 final Iterator<Content> fileListIterator = topContentElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
439 final String uuid = UUIDGenerator.getInstance().generateTimeBasedUUID().toString();
440 final String handle = existingContent.getHandle();
441
442 final HierarchyManager systemHM = MgnlContext.getSystemContext().getHierarchyManager("mgnlSystem");
443 try {
444 while (fileListIterator.hasNext()) {
445 Element fileElement = (Element) fileListIterator.next();
446 importResource(data, fileElement, hierarchyManager, handle);
447 }
448
449 Content activationTmp = ContentUtil.getOrCreateContent(systemHM.getRoot(), "activation-tmp", ItemType.FOLDER, true);
450 final Content transientNode = activationTmp.createContent(uuid, ItemType.CONTENTNODE.toString());
451 final String transientStoreHandle = transientNode.getHandle();
452
453 final String fileName = topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
454 final GZIPInputStream inputStream = new GZIPInputStream(data.getDocument(fileName).getStream());
455
456 systemHM.getWorkspace().getSession().importXML(transientStoreHandle, inputStream, ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW);
457 IOUtils.closeQuietly(inputStream);
458
459 Content tmpContent = transientNode.getContent(topContentElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE));
460 copyProperties(tmpContent, existingContent);
461 systemHM.delete(transientStoreHandle);
462 hierarchyManager.save();
463 systemHM.save();
464 } catch (Exception e) {
465
466 hierarchyManager.refresh(false);
467 systemHM.refresh(false);
468
469 log.error("Exception caught", e);
470 throw new ExchangeException("Activation failed : " + e.getMessage());
471 }
472 }
473
474
475
476
477
478
479
480
481
482 protected synchronized void importResource(MultipartForm data, Element resourceElement, HierarchyManager hm, String parentPath) throws Exception {
483
484
485 Access.isGranted(hm.getAccessManager(), parentPath, Permission.WRITE);
486
487 final String name = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_NAME_ATTRIBUTE);
488 final String fileName = resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_ID_ATTRIBUTE);
489
490 final GZIPInputStream inputStream = new GZIPInputStream(data.getDocument(fileName).getStream());
491 log.debug("Importing {} into parent path {}", new Object[] {name, parentPath});
492 hm.getWorkspace().getSession().importXML(parentPath, inputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
493 IOUtils.closeQuietly(inputStream);
494 Iterator fileListIterator = resourceElement.getChildren(BaseSyndicatorImpl.RESOURCE_MAPPING_FILE_ELEMENT).iterator();
495
496 try {
497 parentPath = hm.getContentByUUID(resourceElement.getAttributeValue(BaseSyndicatorImpl.RESOURCE_MAPPING_UUID_ATTRIBUTE)).getHandle();
498 } catch (ItemNotFoundException e) {
499
500
501 parentPath = StringUtils.removeEnd(parentPath, "/") + "/" + name;
502 }
503 while (fileListIterator.hasNext()) {
504 Element fileElement = (Element) fileListIterator.next();
505 importResource(data, fileElement, hm, parentPath);
506 }
507 }
508
509
510
511
512
513
514 protected synchronized String remove(HttpServletRequest request) throws Exception {
515 HierarchyManager hm = getHierarchyManager(request);
516 String handle = null;
517 try {
518 Content node = this.getNode(request);
519 handle = node.getHandle();
520 hm.delete(handle);
521 hm.save();
522 } catch (ItemNotFoundException e) {
523 log.debug("Unable to delete node", e);
524 }
525 return handle;
526 }
527
528
529
530
531
532
533 protected HierarchyManager getHierarchyManager(HttpServletRequest request) throws ExchangeException {
534 String repositoryName = request.getHeader(BaseSyndicatorImpl.REPOSITORY_NAME);
535 String workspaceName = request.getHeader(BaseSyndicatorImpl.WORKSPACE_NAME);
536
537 if (StringUtils.isEmpty(repositoryName) || StringUtils.isEmpty(workspaceName)) {
538 throw new ExchangeException("Repository or workspace name not sent, unable to activate. Repository: " + repositoryName + ", workspace: " + workspaceName) ;
539 }
540
541 return MgnlContext.getHierarchyManager(repositoryName, workspaceName);
542 }
543
544
545
546
547
548 protected void cleanUp(HttpServletRequest request) {
549 if (BaseSyndicatorImpl.ACTIVATE.equalsIgnoreCase(request.getHeader(BaseSyndicatorImpl.ACTION))) {
550 MultipartForm data = MgnlContext.getPostedForm();
551 if (null != data) {
552 Iterator keys = data.getDocuments().keySet().iterator();
553 while (keys.hasNext()) {
554 String key = (String) keys.next();
555 data.getDocument(key).delete();
556 }
557 }
558 try {
559 final String parentPath = getParentPath(request);
560 if (StringUtils.isEmpty(parentPath) || this.getHierarchyManager(request).isExist(parentPath)) {
561 Content content = this.getNode(request);
562 if (content.isLocked()) {
563 content.unlock();
564 }
565 }
566 } catch (LockException le) {
567
568 log.debug(le.getMessage());
569 } catch (RepositoryException re) {
570
571 log.warn("Exception caught", re);
572 } catch (ExchangeException e) {
573
574 log.warn("Exception caught", e);
575 }
576 }
577
578
579 try {
580 HttpSession httpSession = request.getSession(false);
581 if (httpSession != null) {
582 httpSession.invalidate();
583 }
584 } catch (Throwable t) {
585
586 log.error("failed to invalidate session", t);
587 }
588 }
589
590
591
592
593
594 protected void applyLock(HttpServletRequest request) throws ExchangeException {
595 try {
596 int retries = getUnlockRetries();
597 long retryWait = getRetryWait() * 1000;
598 Content content = this.getNode(request);
599 while (content.isLocked() && retries > 0) {
600 log.info("Content " + content.getHandle() + " is locked. Will retry " + retries + " more times.");
601 try {
602 Thread.sleep(retryWait);
603 } catch (InterruptedException e) {
604
605 Thread.currentThread().interrupt();
606 }
607 retries--;
608 content = this.getNode(request);
609 }
610 if (content.isLocked()) {
611 throw new ExchangeException("Operation not permitted, " + content.getHandle() + " is locked");
612 }
613
614 content.lock(true, true);
615 } catch (LockException le) {
616
617 log.debug(le.getMessage());
618 } catch (ItemNotFoundException e) {
619
620 log.warn("Attempt to lock non existing content {} during (de)activation.",getUUID(request));
621 } catch (PathNotFoundException e) {
622
623 log.debug("Attempt to lock non existing content {}:{} during (de)activation.",getHierarchyManager(request).getName(), getParentPath(request));
624 } catch (RepositoryException re) {
625
626 log.warn("Exception caught", re);
627 }
628 }
629
630 protected Content getNode(HttpServletRequest request) throws ExchangeException, RepositoryException {
631 if (request.getHeader(BaseSyndicatorImpl.PARENT_PATH) != null) {
632 String parentPath = this.getParentPath(request);
633 log.debug("parent path:" + parentPath);
634 return this.getHierarchyManager(request).getContent(parentPath);
635 } else if (!StringUtils.isEmpty(getUUID(request))){
636 log.debug("node uuid:" + request.getHeader(BaseSyndicatorImpl.NODE_UUID));
637 return this.getHierarchyManager(request).getContentByUUID(request.getHeader(BaseSyndicatorImpl.NODE_UUID));
638 }
639
640 else {
641 log.debug("path: {}", request.getHeader(BaseSyndicatorImpl.PATH));
642 return this.getHierarchyManager(request).getContent(request.getHeader(BaseSyndicatorImpl.PATH));
643 }
644 }
645
646 protected String getParentPath(HttpServletRequest request) {
647 String parentPath = request.getHeader(BaseSyndicatorImpl.PARENT_PATH);
648 if (StringUtils.isNotEmpty(parentPath)) {
649 return parentPath;
650 }
651 return "";
652 }
653
654 protected String getUUID(HttpServletRequest request) {
655 String parentPath = request.getHeader(BaseSyndicatorImpl.NODE_UUID);
656 if (StringUtils.isNotEmpty(parentPath)) {
657 return parentPath;
658 }
659 return "";
660 }
661
662
663 }