View Javadoc

1   /**
2    * This file Copyright (c) 2003-2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.workflow.jcr;
35  
36  import info.magnolia.cms.core.HierarchyManager;
37  import info.magnolia.cms.core.ItemType;
38  import info.magnolia.cms.core.search.Query;
39  import info.magnolia.cms.core.search.QueryManager;
40  import info.magnolia.cms.core.search.QueryResult;
41  import info.magnolia.cms.security.AccessDeniedException;
42  import info.magnolia.cms.util.ContentUtil;
43  import info.magnolia.context.LifeTimeJCRSessionUtil;
44  import info.magnolia.context.MgnlContext;
45  import info.magnolia.context.MgnlContext.Op;
46  import info.magnolia.module.workflow.WorkflowConstants;
47  
48  import java.io.InputStream;
49  import java.util.ArrayList;
50  import java.util.Iterator;
51  import java.util.Map;
52  import java.util.NoSuchElementException;
53  
54  import javax.jcr.Node;
55  import javax.jcr.PathNotFoundException;
56  import javax.jcr.RepositoryException;
57  
58  import openwfe.org.ApplicationContext;
59  import openwfe.org.ServiceException;
60  import openwfe.org.engine.expool.PoolException;
61  import openwfe.org.engine.expressions.FlowExpression;
62  import openwfe.org.engine.expressions.FlowExpressionId;
63  import openwfe.org.engine.expressions.raw.RawExpression;
64  import openwfe.org.engine.impl.expool.AbstractExpressionStore;
65  import openwfe.org.util.beancoder.XmlBeanCoder;
66  import openwfe.org.xml.XmlUtils;
67  
68  import org.apache.jackrabbit.commons.iterator.FilteringNodeIterator;
69  import org.apache.jackrabbit.commons.predicate.Predicate;
70  import org.jdom.Document;
71  import org.jdom.input.SAXBuilder;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * The JCR implementation of the expression store.
77   *
78   * @author Jackie Ju
79   * @author Nicolas Modrzyk
80   * @author John Mettraux
81   * @author gjoseph
82   */
83  public class JCRExpressionStore extends AbstractExpressionStore {
84      private static final Logger log = LoggerFactory.getLogger(JCRExpressionStore.class);
85  
86      private static final String ENGINE_ID = "ee";
87  
88      private boolean useLifeTimeJCRSession = true;
89  
90      private boolean cleanUp = false;
91  
92      public JCRExpressionStore(boolean useLifeTimeJCRSession, boolean cleanUp) {
93          super();
94          this.useLifeTimeJCRSession = useLifeTimeJCRSession;
95          this.cleanUp = cleanUp;
96      }
97  
98      @Override
99      public void init(final String serviceName, final ApplicationContext context, final Map serviceParams) throws ServiceException {
100         super.init(serviceName, context, serviceParams);
101     }
102 
103     /**
104      * Stores one expression.
105      */
106     @Override
107     public synchronized void storeExpression(final FlowExpression fe) throws PoolException {
108         boolean release = !useLifeTimeJCRSession && !MgnlContext.hasInstance();
109         HierarchyManager hm = null;
110         try {
111             hm = getHierarchyManager();
112             final Node cExpression = findOrCreateExpression(fe, hm);
113 
114             log.debug("storeExpression() handle is " + cExpression.getPath());
115 
116             MgnlContext.doInSystemContext(new Op<Void, RepositoryException>() {
117                 @Override
118                 public Void exec() throws RepositoryException {
119                     // set expressionId as attribte id
120                     String value = fe.getId().toParseableString();
121 
122                     cExpression.setProperty(WorkflowConstants.NODEDATA_ID, value);
123                     return null;
124                 }
125             }, true);
126 
127             serializeExpressionAsXml(cExpression, fe);
128 
129             hm.save();
130         } catch (Exception e) {
131             log.error("storeExpression() store exception failed", e);
132             try {
133                 if (hm != null && hm.hasPendingChanges()) {
134                     hm.refresh(true);
135                 }
136             } catch (RepositoryException e1) {
137                 log.error("Corrupted HM during WKF access", e);
138             }
139             throw new PoolException("storeExpression() store exception failed", e);
140         } finally {
141             if (release) {
142                 MgnlContext.release();
143             }
144         }
145     }
146 
147     /**
148      * Removes the expression from the JCR storage.
149      */
150     @Override
151     public synchronized void unstoreExpression(final FlowExpression fe) throws PoolException {
152         boolean release = !useLifeTimeJCRSession && !MgnlContext.hasInstance();
153         try {
154             final HierarchyManager hm = getHierarchyManager();
155             final Node cExpression = findOrCreateExpression(fe, hm);
156 
157             if (cExpression != null) {
158                 if (cleanUp) {
159                     MgnlContext.doInSystemContext(new Op<Void, RepositoryException>() {
160                         @Override
161                         public Void exec() throws RepositoryException {
162                             deleteAndRemoveEmptyParents(cExpression, 1);
163                             return null;
164                         }
165                     }, true);
166                 } else {
167                     cExpression.remove();
168                 }
169                 hm.save();
170             } else {
171                 log.info("unstoreExpression() " + "didn't find content node for fe " + fe.getId().toParseableString());
172             }
173         } catch (Exception e) {
174             log.error("unstoreExpression() unstore exception failed", e);
175             throw new PoolException("unstoreExpression() unstore exception failed", e);
176         } finally {
177             if (release) {
178                 MgnlContext.release();
179             }
180         }
181     }
182 
183     public static void deleteAndRemoveEmptyParents(Node node, int level) throws PathNotFoundException, RepositoryException,
184     AccessDeniedException {
185         Node parent = null;
186         if (node.getDepth() != 0) {
187             parent = node.getParent();
188         }
189         node.remove();
190         if (parent != null && parent.getDepth() > level && new FilteringNodeIterator(parent.getNodes(), new Predicate() {
191 
192             @Override
193             public boolean evaluate(Object obj) {
194                 if (obj instanceof Node) {
195                     Node content = (Node) obj;
196                     try {
197                         return !content.getName().startsWith("jcr:") && !content.isNodeType(ItemType.NT_METADATA);
198                     } catch (RepositoryException e) {
199                         return false;
200                     }
201                 }
202                 return false;
203             }
204         }).getSize() == 0) {
205             deleteAndRemoveEmptyParents(parent, level);
206         }
207     }
208 
209     /**
210      * Returns an iterator on the content of that expression store.
211      */
212     @Override
213     public synchronized Iterator contentIterator(final Class assignClass) {
214         try {
215             return new StoreIterator(assignClass);
216         } catch (final Throwable t) {
217             log.error("contentIterator() failed to set up an iterator", t);
218         }
219 
220         // TODO : does this need Iterator need to be modifiable? otherwise just
221         // return Collections.emptyList()
222         // return null;
223         return new ArrayList(0).iterator();
224     }
225 
226     /**
227      * Loads an expression given its id.
228      */
229     @Override
230     public synchronized FlowExpression loadExpression(final FlowExpressionId fei) throws PoolException {
231         try {
232             Node cExpression = findExpression(fei, getHierarchyManager());
233 
234             if (cExpression != null) {
235                 final FlowExpression expression = deserializeExpressionAsXml(cExpression);
236                 if (expression != null) {
237                     expression.setApplicationContext(getContext());
238                     return expression;
239                 }
240             }
241         } catch (final Exception e) {
242             log.error("loadExpression() failed for " + fei.asStringId(), e);
243 
244             throw new PoolException("loadExpression() failed for " + fei.asStringId(), e);
245         }
246 
247         // this is normal after clean installation or manual cleanup of the expressions workspace
248         log.info("Expected expression " + fei.asStringId() + " was not found in the repository.");
249 
250         throw new PoolException("loadExpression() " + "didn't find expression " + fei.asStringId() + " in the repository");
251     }
252 
253     /**
254      * Returns the number of expressions currently stored in that store.
255      */
256     @Override
257     public int size() {
258         try {
259             final QueryManager qm = MgnlContext.getSystemContext().getQueryManager(WorkflowConstants.WORKSPACE_EXPRESSION);
260             Query q = qm.createQuery(WorkflowConstants.STORE_ITERATOR_QUERY, Query.SQL);
261             QueryResult qr = q.execute();
262 
263             return qr.getContent().size();
264         } catch (final Exception e) {
265             log.error("size() failed", e);
266             return -1;
267         }
268     }
269 
270     private void serializeExpressionAsXml(final Node c, FlowExpression fe) throws Exception {
271         final org.jdom.Document doc = XmlBeanCoder.xmlEncode(fe);
272         final String s = XmlUtils.toString(doc, null);
273 
274         MgnlContext.doInSystemContext(new Op<Void, RepositoryException>() {
275             @Override
276             public Void exec() throws RepositoryException {
277                 c.setProperty(WorkflowConstants.NODEDATA_VALUE, s);
278                 return null;
279             }
280         }, true);
281     }
282 
283     public final String toXPathFriendlyString(final FlowExpressionId fei) {
284         final StringBuffer buffer = new StringBuffer();
285         final String engineId = fei.getEngineId();
286 
287         buffer.append(WorkflowConstants.SLASH);
288         buffer.append(engineId);
289 
290         // engine storage
291         if (engineId.equals(ENGINE_ID)) {
292             return buffer.toString();
293         }
294 
295         buffer.append(WorkflowConstants.SLASH);
296         buffer.append(fei.getWorkflowDefinitionName());
297 
298         buffer.append(WorkflowConstants.SLASH);
299         buffer.append(fei.getWorkflowInstanceId());
300 
301         buffer.append(WorkflowConstants.SLASH);
302         buffer.append(fei.getExpressionId());
303         buffer.append("__");
304         buffer.append(fei.getExpressionName());
305 
306         return buffer.toString();
307     }
308 
309     private Node findOrCreateExpression(final FlowExpression fe, HierarchyManager hm) throws Exception {
310         Node content = findExpression(fe.getId(), hm);
311         if (content == null) {
312             content = ContentUtil.createPath(hm, toXPathFriendlyString(fe.getId()), ItemType.EXPRESSION).getJCRNode();
313         }
314         return content;
315     }
316 
317     private Node findExpression(final FlowExpressionId fei, HierarchyManager hm) throws Exception {
318         if (log.isDebugEnabled()) {
319             log.debug("findExpression() looking for " + fei.toParseableString());
320         }
321 
322         final String path = toXPathFriendlyString(fei);
323 
324         if (hm.isExist(path)) {
325             return hm.getContent(path).getJCRNode();
326         } else {
327             return null;
328         }
329     }
330 
331     private FlowExpression deserializeExpressionAsXml(final Node c) throws Exception {
332         if (!c.hasProperty(WorkflowConstants.NODEDATA_VALUE)) {
333             return null;
334         }
335         final InputStream is = c.getProperty(WorkflowConstants.NODEDATA_VALUE).getStream();
336 
337         final SAXBuilder builder = new SAXBuilder();
338         final Document doc = builder.build(is);
339         return (FlowExpression) XmlBeanCoder.xmlDecode(doc);
340     }
341 
342     protected HierarchyManager getHierarchyManager() {
343         HierarchyManager hm;
344         if (useLifeTimeJCRSession) {
345             hm = LifeTimeJCRSessionUtil.getHierarchyManager(WorkflowConstants.WORKSPACE_EXPRESSION);
346         } else {
347             hm = MgnlContext.getSystemContext().getHierarchyManager(WorkflowConstants.WORKSPACE_EXPRESSION);
348         }
349         try {
350             if (hm.hasPendingChanges()) {
351                 // If this happens it might be related to MAGNOLIA-2172
352                 // the methods of the expression store are synchronized so this
353                 // should not happen!
354                 log.warn("The workflow expression session has pending changes while " + (useLifeTimeJCRSession ? "" : "not ") + "using Life Time session. Will clean the session",
355                         new Exception());
356                 hm.refresh(true);
357             }
358         } catch (RepositoryException e) {
359             // should really not happen
360             log.error("Can't check/refresh worflow expression session.", e);
361         }
362         return hm;
363     }
364 
365     /**
366      * 'lightweight' storeIterator. The previous version were stuffing all the
367      * expression within a collection and returning an iterator on it.
368      * <p>
369      * The remaining question is : what's behind Magnolia's Content.iterator()
370      * method ?
371      */
372     protected final class StoreIterator implements Iterator {
373         private final Class assignClass;
374         private Iterator rootIterator = null;
375         private FlowExpression next = null;
376 
377         public StoreIterator(final Class assignClass) throws Exception {
378             super();
379 
380             this.assignClass = assignClass;
381 
382             javax.jcr.query.QueryManager qm = LifeTimeJCRSessionUtil.getHierarchyManager(WorkflowConstants.WORKSPACE_EXPRESSION).getWorkspace().getQueryManager();
383 
384             final javax.jcr.query.Query query = qm.createQuery(WorkflowConstants.STORE_ITERATOR_QUERY, Query.SQL);
385 
386             final javax.jcr.query.QueryResult qr = query.execute();
387 
388             if (log.isDebugEnabled()) {
389                 log.debug("() query found " + qr.getNodes().getSize() + " elements");
390             }
391 
392             this.rootIterator = qr.getNodes();
393 
394             this.next = fetchNext();
395         }
396 
397         @Override
398         public boolean hasNext() {
399             return (this.next != null);
400         }
401 
402         public FlowExpression fetchNext() {
403             if (!this.rootIterator.hasNext()) {
404                 return null;
405             }
406 
407             FlowExpression fe = null;
408             do {
409                 fe = fetchMaybeNext();
410 
411                 if (fe != null) {
412                     if (isAssignableFromClass(fe, this.assignClass)) {
413                         return fe;
414                     } else {
415                         fe = null;
416                     }
417                 }
418 
419                 if (!this.rootIterator.hasNext()) {
420                     // nothing else to iterate over
421                     return null;
422                 }
423             } while (fe == null);
424             return null;
425         }
426 
427         private FlowExpression fetchMaybeNext() {
428             if (!this.rootIterator.hasNext()) {
429                 return null;
430             }
431 
432             final Node content = (Node) this.rootIterator.next();
433             try {
434                 final FlowExpression fe = deserializeExpressionAsXml(content);
435 
436                 if (fe == null) {
437                     return null;
438                 }
439 
440                 fe.setApplicationContext(getContext());
441 
442 
443                 return fe;
444             } catch (final Exception e) {
445                 log.error("fetchNext() problem", e);
446                 return null;
447             }
448         }
449 
450         @Override
451         public Object next() throws java.util.NoSuchElementException {
452             final FlowExpression current = this.next;
453 
454             if (current == null) {
455                 throw new NoSuchElementException();
456             }
457 
458             this.next = fetchNext();
459 
460             if (log.isDebugEnabled()) {
461                 log.debug("next() is  " + (next != null ? next.getId().toString() : "'null'"));
462             }
463 
464             return current;
465         }
466 
467         @Override
468         public void remove() {
469             throw new UnsupportedOperationException();
470         }
471 
472         // TODO : this was copied from ExpoolUtils, adding a fix for
473         // MAGNOLIA-1131
474         private boolean isAssignableFromClass(final FlowExpression fe, final Class expClass) {
475             if (expClass == null) {
476                 return true;
477             }
478 
479 
480             if (fe instanceof RawExpression) {
481                 Class c = fe.getExpressionClass();
482                 if (c == null) {
483                     // TODO : fe.getDefinitionName() does not return the xml's
484                     // root name as I expected ... (but its name attribute
485                     // instead)
486                     log.warn("Skipping expression " + fe.getId() + " (" + ((RawExpression) fe).getDefinitionName() + ")");
487                     return false;
488                 }
489             }
490 
491             Class c = fe.getClass();
492             return expClass.isAssignableFrom(c);
493         }
494     }
495 
496 }