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