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