View Javadoc

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