View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 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.context;
35  
36  import info.magnolia.cms.beans.runtime.MultipartForm;
37  import info.magnolia.cms.core.AggregationState;
38  import info.magnolia.cms.core.HierarchyManager;
39  import info.magnolia.cms.core.search.QueryManager;
40  import info.magnolia.cms.i18n.Messages;
41  import info.magnolia.cms.security.AccessManager;
42  import info.magnolia.cms.security.User;
43  
44  import java.util.Locale;
45  import java.util.Map;
46  
47  import javax.jcr.InvalidItemStateException;
48  import javax.jcr.LoginException;
49  import javax.jcr.Node;
50  import javax.jcr.RepositoryException;
51  import javax.jcr.Session;
52  import javax.jcr.lock.LockException;
53  import javax.jcr.lock.LockManager;
54  import javax.security.auth.Subject;
55  import javax.servlet.ServletContext;
56  import javax.servlet.http.HttpServletRequest;
57  import javax.servlet.http.HttpServletResponse;
58  
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  
63  /**
64   * This class allows obtaining of the current Request without passing the request around the world. A ThreadLocal variable is used to
65   * manage the request and to make sure it doesn't escape to another processing.
66   * <p>
67   * In a later version this class should not depend on servlets. The core should use the context to get and set
68   * attributes instead of using the request or session object directly. Magnolia could run then in a neutral and
69   * configurable context.
70   */
71  
72  public class MgnlContext {
73      private static final Logger log = LoggerFactory.getLogger(MgnlContext.class);
74  
75      /**
76       * The thread local variable holding the current context.
77       */
78      private static ThreadLocal<Context> localContext = new ThreadLocal<Context>();
79  
80      /**
81       * Do not instantiate this class. The constructor must be public to use discovery
82       */
83      public MgnlContext() {
84      }
85  
86      /**
87       * A short cut for the current user.
88       *
89       * @return the current user
90       */
91      public static User getUser() {
92          return getInstance().getUser();
93      }
94  
95      /**
96       * Set the locale for the current context.
97       */
98      public static void setLocale(Locale locale) {
99          getInstance().setLocale(locale);
100     }
101 
102     /**
103      * Get the context's locale object.
104      *
105      * @return the current locale
106      */
107     public static Locale getLocale() {
108         return getInstance().getLocale();
109     }
110 
111     public static Messages getMessages() {
112         return getInstance().getMessages();
113     }
114 
115     public static Messages getMessages(String basename) {
116         return getInstance().getMessages(basename);
117     }
118 
119     public static void login(Subject subject) {
120         ((UserContext) getInstance()).login(subject);
121     }
122 
123     /**
124      * Get hierarchy manager initialized for this user.
125      *
126      * @deprecated since 4.5 - use {@link #getJCRSession(String)}
127      */
128     @Deprecated
129     public static HierarchyManager getHierarchyManager(String repositoryId) {
130         return getInstance().getHierarchyManager(repositoryId);
131     }
132 
133     /**
134      * Get access manager for a resource, usually a workspace.
135      */
136     public static AccessManager getAccessManager(String name) {
137         return getInstance().getAccessManager(name);
138     }
139 
140     /**
141      * Get access manager for the specified repository on the specified workspace.
142      *
143      * @deprecated since 4.5 - use {@link #getJCRSession(String)} and acquire the JCR query manager directly from the session.
144      */
145     @Deprecated
146     public static QueryManager getQueryManager(String workspaceName) {
147         return getInstance().getQueryManager(workspaceName);
148     }
149 
150     /**
151      * Get form object assembled by <code>MultipartRequestFilter</code>.
152      *
153      * @return multipart form object
154      *         TODO - move to getAggregationState() ?
155      */
156     public static MultipartForm getPostedForm() {
157         WebContext ctx = getWebContextOrNull();
158         if (ctx != null) {
159             return ctx.getPostedForm();
160         }
161         return null;
162     }
163 
164     /**
165      * Get parameter value as string.
166      */
167     public static String getParameter(String name) {
168         WebContext ctx = getWebContextOrNull();
169         if (ctx != null) {
170             return ctx.getParameter(name);
171         }
172         return null;
173 
174     }
175 
176     public static String[] getParameterValues(String name) {
177         WebContext ctx = getWebContextOrNull();
178         if (ctx != null) {
179             return ctx.getParameterValues(name);
180         }
181         return null;
182 
183     }
184 
185     /**
186      * Get parameter value as a Map&lt;String, String&gt;.
187      */
188     public static Map<String, String> getParameters() {
189         WebContext ctx = getWebContextOrNull();
190         if (ctx != null) {
191             return ctx.getParameters();
192         }
193         return null;
194     }
195 
196     /**
197      * @return the context path.
198      */
199     public static String getContextPath() {
200         WebContext ctx = getWebContextOrNull();
201         if (ctx != null) {
202             return ctx.getContextPath();
203         }
204         throw new IllegalStateException("Can only get the context path within a WebContext.");
205     }
206 
207     /**
208      * Returns the AggregationState if we're in a WebContext, throws an
209      * IllegalStateException otherwise.
210      */
211     public static AggregationState getAggregationState() {
212         final WebContext ctx = getWebContextOrNull();
213         if (ctx != null) {
214             return ctx.getAggregationState();
215         }
216         throw new IllegalStateException("Can only get the aggregation state within a WebContext.");
217     }
218 
219     /**
220      * Resets the current aggregation state if we're in a WebContext, throws an IllegalStateException otherwise.
221      */
222     public static void resetAggregationState() {
223         final WebContext ctx = getWebContextOrNull();
224         if (ctx != null) {
225             ctx.resetAggregationState();
226         } else {
227             throw new IllegalStateException("Can only reset the aggregation state within a WebContext.");
228         }
229     }
230 
231     /**
232      * Set attribute value, scope of the attribute is {@link Context#LOCAL_SCOPE}.
233      */
234     public static void setAttribute(String name, Object value) {
235         getInstance().setAttribute(name, value, Context.LOCAL_SCOPE);
236     }
237 
238     /**
239      * Set attribute value, scope of the attribute is defined.
240      *
241      * @param scope , highest level of scope from which this attribute is visible.
242      */
243     public static void setAttribute(String name, Object value, int scope) {
244         getInstance().setAttribute(name, value, scope);
245     }
246 
247     /**
248      * Get attribute value.
249      */
250     public static <T> T getAttribute(String name) {
251         return (T) getInstance().getAttribute(name);
252     }
253 
254     /**
255      * Get the attribute from the specified scope.
256      */
257     public static <T> T getAttribute(String name, int scope) {
258         return (T) getInstance().getAttribute(name, scope);
259     }
260 
261     /**
262      * Check if this attribute exists in the local scope.
263      */
264     public static boolean hasAttribute(String name) {
265         return getInstance().getAttribute(name, Context.LOCAL_SCOPE) != null;
266     }
267 
268     /**
269      * Check if this attribute exists in the specified scope.
270      */
271     public static boolean hasAttribute(String name, int scope) {
272         return getInstance().getAttribute(name, scope) != null;
273     }
274 
275     /**
276      * Remove an attribute in the local scope.
277      */
278     public static void removeAttribute(String name) {
279         getInstance().removeAttribute(name, Context.LOCAL_SCOPE);
280     }
281 
282     /**
283      * Remove an attribute in the specified scope.
284      */
285     public static void removeAttribute(String name, int scope) {
286         getInstance().removeAttribute(name, scope);
287     }
288 
289     /**
290      * Set context implementation instance.
291      */
292     public static void setInstance(Context context) {
293         localContext.set(context);
294     }
295 
296     /**
297      * Get the current context of this thread.
298      */
299     public static Context getInstance() {
300         Context context = localContext.get();
301         // It should never fall back, We need to fix all false callers instead
302         if (context == null) {
303             final IllegalStateException ise = new IllegalStateException("MgnlContext is not set for this thread");
304             log.error("MgnlContext is not initialized. This could happen if the request does not go through the Magnolia default filters.", ise);
305             throw ise;
306         }
307         return context;
308     }
309 
310     /**
311      * Throws an IllegalStateException if the current context is not set, or if it is not an instance of WebContext.
312      *
313      * @see #getWebContext(String)
314      */
315     public static WebContext getWebContext() {
316         return getWebContext(null);
317     }
318 
319     /**
320      * Throws an IllegalStateException if the current context is not set, or if it is not an instance of WebContext.
321      * Yes, you can specify the exception message if you want. This is useful if you're calling this from a component
322      * which only supports WebContext and still care enough to actually throw an exception with a meaningful message.
323      *
324      * @see #getWebContext()
325      */
326     public static WebContext getWebContext(String exceptionMessage) {
327         final WebContext wc = getWebContextIfExisting(getInstance());
328         if (wc == null) {
329             throw new IllegalStateException(exceptionMessage == null ? "The current context is not an instance of WebContext (" + localContext.get() + ")" : exceptionMessage);
330         }
331         return wc;
332     }
333 
334     /**
335      * Returns the current context if it is set and is an instance of WebContext, returns null otherwise.
336      */
337     public static WebContext getWebContextOrNull() {
338         return hasInstance() ? getWebContextIfExisting(getInstance()) : null;
339     }
340 
341     /**
342      * Used to check if an instance is already set since getInstance() will always return a context.
343      *
344      * @return true if an instance was set.
345      */
346     public static boolean hasInstance() {
347         return localContext.get() != null;
348     }
349 
350     public static boolean isSystemInstance() {
351         return localContext.get() instanceof SystemContext;
352     }
353 
354     /**
355      * Returns true if the current context is set and is an instance of WebContext. (it might be wrapped in a ContextDecorator)
356      */
357     public static boolean isWebContext() {
358         return hasInstance() && getWebContextIfExisting(getInstance()) != null;
359     }
360 
361     /**
362      * Get Magnolia system context. This context has full permissions over all repositories/ workspaces.
363      *
364      * @deprecated since 4.5, use IoC, i.e., declare a dependency on SystemContext in your component.
365      */
366     @Deprecated
367     public static SystemContext getSystemContext() {
368         return ContextFactory.getInstance().getSystemContext();
369     }
370 
371     /**
372      * Executes the given operation in the system context and sets it back to the original once done
373      * (also if an exception is thrown). Also works if there was no context upon calling. (sets it back
374      * to null in this case)
375      */
376     public static <T, E extends Throwable> T doInSystemContext(final Op<T, E> op) throws E {
377         return doInSystemContext(op, false);
378     }
379 
380     /**
381      * Executes the given operation in the system context and sets it back to the original once done
382      * (also if an exception is thrown). Also works if there was no context upon calling (sets it back
383      * to null in this case)
384      *
385      * @param releaseAfterExecution set to true if the context should be released once the execution is done (e.g. in workflow operations or scheduled jobs).
386      */
387     public static <T, E extends Throwable> T doInSystemContext(final Op<T, E> op, boolean releaseAfterExecution) throws E {
388         final Context originalCtx = MgnlContext.hasInstance() ? MgnlContext.getInstance() : null;
389         T result;
390         try {
391             MgnlContext.setInstance(MgnlContext.getSystemContext());
392             result = op.exec();
393             if (releaseAfterExecution) {
394                 MgnlContext.release();
395             }
396         } finally {
397             MgnlContext.setInstance(originalCtx);
398         }
399         return result;
400     }
401 
402     /**
403      * A simple execution interface to be used with the doInSystemContext method.
404      * If no return value is necessary, return null (for semantic's sake, declare T as <Void>)
405      * If no checked exception need to be thrown, declare E as <RuntimeException>)
406      *
407      * @param <T> the return type of this operation
408      * @param <E> an exception this operation can throw
409      * @see MgnlContext#doInSystemContext(Op op)
410      */
411     public static interface Op<T, E extends Throwable> {
412         T exec() throws E;
413     }
414 
415     /**
416      * An Op that does not return values and can only throw RuntimeExceptions.
417      */
418     public abstract static class VoidOp implements Op<Void, RuntimeException> {
419         @Override
420         public Void exec() {
421             doExec();
422             return null;
423         }
424 
425         abstract public void doExec();
426     }
427 
428     /**
429      * An Op that does not return values and can only throw RepositoryExceptions.
430      */
431     public abstract static class RepositoryOp implements Op<Void, RepositoryException> {
432         @Override
433         public Void exec() throws RepositoryException {
434             doExec();
435             return null;
436         }
437 
438         abstract public void doExec() throws RepositoryException;
439     }
440 
441     /**
442      * Operation that is performed only if JCR lock can be issued on the path specified by combination of parameters in constructor. Lock issued and held by this operation prevents other concurrent modifications and should not be used without reason. Locking is always session scoped, meaning that is issuing session is closed, any locks held by the session are forcefully released at this point. Since Magnolia uses request scoped sessions in most cases, it means that no matter what happens lock will be released latest when request processing is finished.
443      */
444     public abstract static class LockingOp extends RepositoryOp {
445 
446         private final long sleepTime = 200;
447         private final int maxAttempts = 50;
448 
449         final private String workspaceName;
450         final private String lockPath;
451         final private String userName;
452         final private boolean deepLock;
453         final private String lockedNodeType;
454 
455         /**
456          * Performs operation only if shallow lock can be obtained within reasonable time on a path specified by params.
457          */
458         public LockingOp(String workspaceName, String lockPath) {
459             this(workspaceName, lockPath, false);
460         }
461 
462         /**
463          * Performs operation only if shallow or deep lock can be obtained within reasonable time on a path specified by params.
464          * Use deep locking with care as it will stop all other locking operations all the way down your tree structure. Obtaining and releasing deep lock is also more expensive then shallow locking.
465          */
466         public LockingOp(String workspaceName, String lockPath, boolean deepLock) {
467             this(workspaceName, lockPath, deepLock, null);
468         }
469 
470         /**
471          * Performs operation only if shallow lock can be obtained within reasonable time on a path specified by params.
472          * If node at specified path is not of specified primary type, this action will attempt to locate and lock nearest parent node that is of specified type. Failing to locate such node, lock will be issued on the specified path itself.
473          */
474         public LockingOp(String workspaceName, String lockPath, String nodeType) {
475             // since this is used in constructor the call should be done still prior the use of system context and we should still get real user
476             this(workspaceName, lockPath, false, nodeType, MgnlContext.getUser() == null ? "not available" : MgnlContext.getUser().getName());
477         }
478 
479         /**
480          * Performs operation only if shallow or deep lock can be obtained within reasonable time on a path specified by params.
481          * If node at specified path is not of specified primary type, this action will attempt to locate and lock nearest parent node that is of specified type. Failing to locate such node, lock will be issued on the specified path itself.
482          * Use deep locking with care as it will stop all other locking operations all the way down your tree structure. Obtaining and releasing deep lock is also more expensive then shallow locking.
483          */
484         public LockingOp(String workspaceName, String lockPath, boolean deepLock, String nodeType) {
485             // since this is used in constructor the call should be done still prior the use of system context and we should still get real user
486             this(workspaceName, lockPath, deepLock, nodeType, MgnlContext.getUser() == null ? "not available" : MgnlContext.getUser().getName());
487         }
488 
489         /**
490          * Performs operation only if shallow or deep lock can be obtained within reasonable time on a path specified by params.
491          * If node at specified path is not of specified primary type, this action will attempt to locate and lock nearest parent node that is of specified type. Failing to locate such node, lock will be issued on the specified path itself.
492          * Use deep locking with care as it will stop all other locking operations all the way down your tree structure. Obtaining and releasing deep lock is also more expensive then shallow locking.
493          * Specify a user name to be used in lock description in case this operation should be assigned to other user then one currently logged in (doesn't have any effect on permissions, information is stored for debugging and troubleshooting purposes only).
494          */
495         public LockingOp(String workspaceName, String lockPath, boolean deepLock, String lockedNodeType, String userName) {
496             this.workspaceName = workspaceName;
497             this.userName = userName;
498             this.deepLock = deepLock;
499             this.lockedNodeType = lockedNodeType;
500             this.lockPath = lockPath;
501         }
502 
503         private String getLockPath() throws RepositoryException {
504             if (lockedNodeType == null) {
505                 return lockPath;
506             }
507             Session sysSession = MgnlContext.getJCRSession(workspaceName);
508             Node parentNode = sysSession.getNode(lockPath);
509             Node lockNode = parentNode;
510             while (lockNode != null && lockNode.getDepth() > 0 && !lockedNodeType.equals(lockNode.getPrimaryNodeType().getName())) {
511                 lockNode = lockNode.getParent();
512             }
513 
514             final String lockPath;
515             if (lockNode == null || lockNode.getDepth() == 0) {
516                 if (parentNode == null) {
517                     throw new RepositoryException("Can't perform locking operaion without the locked node. " + workspaceName + ":" + this.lockPath + " doesn't exist.");
518                 }
519                 // root node is not lockable
520                 lockPath = parentNode.getPath();
521                 log.info("Failed to find page path for {}:{}", workspaceName, lockPath);
522             } else {
523                 lockPath = lockNode.getPath();
524             }
525             return lockPath;
526         }
527 
528         @Override
529         public Void exec() throws RepositoryException {
530             final String threadName = Thread.currentThread().getName();
531             final String lockPath = getLockPath();
532             final Session sysSession = MgnlContext.getJCRSession(workspaceName);
533             final String sessionName = sysSession.toString();
534             final LockManager guard = sysSession.getWorkspace().getLockManager();
535             // check if someone else is trying to do something here and lock if not
536             int attempts = maxAttempts;
537             while (attempts > 0) {
538                 if (!guard.isLocked(lockPath)) {
539                     try {
540                         Node node = sysSession.getNode(lockPath);
541                         if (!node.isNodeType("mix:lockable")) {
542                             node.addMixin("mix:lockable");
543                         }
544                         node.save();
545                         guard.lock(lockPath, deepLock, true, sleepTime * maxAttempts, "Lock guarded node update requested by " + userName);
546                         log.debug("Locked {} from {}:{}", lockPath, sessionName, threadName);
547                         break;
548                     } catch (LockException e) {
549                         log.debug("this should happen very rarely. If you are seeing this message something is probably very wrong.", e);
550                         // someone was faster, let's retry
551                     }
552                 }
553                 try {
554                     Thread.sleep(sleepTime);
555                 } catch (InterruptedException e) {
556                     Thread.interrupted();
557                 }
558                 attempts--;
559             }
560             if (attempts == 0) {
561                 log.info("Lock on {} is already held by {}", lockPath, guard.getLock(lockPath));
562                 // we've exhausted all time we got.
563                 String message = "Failed to obtain lock by " + userName + "(" + threadName + ") on " + workspaceName + ":" + lockPath + " within " + ((maxAttempts * sleepTime) / 1000) + " seconds. Will NOT execute operation that requires locking.";
564                 throw new LockException(message);
565             }
566             long timestamp = System.nanoTime();
567             try {
568                 doExec();
569             } catch (LockException e) {
570                 String failedPath = e.getFailureNodePath();
571                 // we locked the node so either someone invoking the op has requested lock on wrong path or is using wrong session ... or something is seriously screwed up
572                 log.error("Lock exception while updating node [{}] supposedly guarded by lock. is it really locked? {}, we hold the lock? {} ... as for [{}] is it locked? {}, we hold the lock? {}", failedPath, "" + guard.isLocked(failedPath), "" + guard.holdsLock(failedPath), lockPath, "" + guard.isLocked(lockPath), "" + guard.holdsLock(lockPath));
573 
574             } finally {
575                 timestamp = System.nanoTime() - timestamp;
576                 // 2 seconds
577                 if (timestamp > 2000000000L) {
578                     log.warn("Lock guarded operation on {}:{}:{} performed by {}:{} took {} seconds to execute. Performance of your server might be sub-optimal.", sessionName, workspaceName, lockPath, userName, threadName, "" + (timestamp / 1000000000L));
579                 }
580                 // always unlock
581                 log.debug("Unocking {} from {}:{}", lockPath, sessionName, threadName);
582                 try {
583                     guard.unlock(lockPath);
584                 } catch (InvalidItemStateException e) {
585                     log.error("Failed to unlock {} from {}:{} with {}. Will attempt to save the session and try again.", lockPath, sessionName, threadName, e.getMessage(), e);
586                     sysSession.save();
587                     guard.unlock(lockPath);
588                 }
589             }
590             return null;
591         }
592     }
593 
594     /**
595      * Sets this context as a web context.
596      *
597      * @param request HttpServletRequest
598      * @param response HttpServletResponse
599      * @param servletContext ServletContext instance
600      * @deprecated since 4.5, use WebContextFactory.
601      */
602     @Deprecated
603     public static void initAsWebContext(HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) {
604         WebContext ctx = ContextFactory.getInstance().createWebContext(request, response, servletContext);
605         setInstance(ctx);
606     }
607 
608     /**
609      * Returns the web context, also if eventually wrapped in a ContextDecorator.
610      *
611      * @return WebContext instance or null if no web context is set
612      */
613     private static WebContext getWebContextIfExisting(Context ctx) {
614         if (ctx instanceof WebContext) {
615             return (WebContext) ctx;
616         } else if (ctx instanceof ContextDecorator) {
617             return getWebContextIfExisting(((ContextDecorator) ctx).getWrappedContext());
618         }
619         return null;
620     }
621 
622     /**
623      * Releases the current thread (if not a system context) and calls the releaseThread() method of the system context.
624      */
625     public static void release() {
626         if (hasInstance() && !(getInstance() instanceof SystemContext)) {
627             getInstance().release();
628         }
629         SystemContext systemContext = getSystemContext();
630         if (systemContext instanceof ThreadDependentSystemContext) {
631             ((ThreadDependentSystemContext) systemContext).releaseThread();
632         }
633     }
634 
635     public static void push(HttpServletRequest request, HttpServletResponse response) {
636         if (isWebContext()) {
637             WebContext wc = getWebContext();
638             wc.push(request, response);
639         }
640     }
641 
642     public static void pop() {
643         if (isWebContext()) {
644             WebContext wc = getWebContext();
645             wc.pop();
646         }
647     }
648 
649     /**
650      * Note: this is the way to go, if you no longer want to rely on the Content-API.
651      *
652      * @param workspaceName - repository to get session for
653      * @return a JCR session to the provided repository
654      */
655     public static Session getJCRSession(String workspaceName) throws LoginException, RepositoryException {
656         return getInstance().getJCRSession(workspaceName);
657     }
658 
659     public static Subject getSubject() {
660         return getInstance().getSubject();
661     }
662 }