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