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