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