001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.jsp;
029
030import org.opencms.db.CmsSubscriptionFilter;
031import org.opencms.db.CmsSubscriptionReadMode;
032import org.opencms.db.CmsVisitedByFilter;
033import org.opencms.file.CmsGroup;
034import org.opencms.file.CmsObject;
035import org.opencms.file.CmsResource;
036import org.opencms.file.CmsUser;
037import org.opencms.flex.CmsFlexController;
038import org.opencms.main.CmsException;
039import org.opencms.main.CmsLog;
040import org.opencms.main.OpenCms;
041import org.opencms.util.CmsStringUtil;
042
043import java.util.ArrayList;
044import java.util.Arrays;
045import java.util.Enumeration;
046import java.util.Iterator;
047import java.util.List;
048
049import javax.servlet.ServletRequest;
050import javax.servlet.http.HttpServletRequest;
051import javax.servlet.http.HttpSession;
052import javax.servlet.jsp.JspException;
053import javax.servlet.jsp.tagext.TagSupport;
054
055import org.apache.commons.logging.Log;
056
057/**
058 * Implementation of the <code>&lt;cms:usertracking/&gt;</code> tag.<p>
059 *
060 * This tag can be used to mark OpenCms files as visited or subscribe/unsubscribe them to/from users or groups.<p>
061 *
062 * It is also possible to check if single resources are visited/subscribed by the current user.<p>
063 *
064 * See also the {@link org.opencms.db.CmsSubscriptionManager} for more information about subscription or visitation.<p>
065 *
066 * @since 8.0
067 */
068public class CmsJspTagUserTracking extends TagSupport {
069
070    /** Prefix for the visited session attributes. */
071    public static final String SESSION_PREFIX_SUBSCRIBED = "__ocmssubscribed_";
072
073    /** Prefix for the visited session attributes. */
074    public static final String SESSION_PREFIX_VISITED = "__ocmsvisited_";
075
076    /** The log object for this class. */
077    private static final Log LOG = CmsLog.getLog(CmsJspTagUserTracking.class);
078
079    /** Serial version UID required for safe serialization. */
080    private static final long serialVersionUID = 4253583631739670341L;
081
082    /** Static array with allowed track action values. */
083    private static final String[] TAG_ACTIONS = {
084        "visit", // 0, default action
085        "subscribe", // 1
086        "unsubscribe", // 2
087        "checkvisited", // 3
088        "checksubscribed" // 4
089    };
090
091    /** List of allowed track action values for more convenient lookup. */
092    private static final List<String> TRACK_ACTIONS_LIST = Arrays.asList(TAG_ACTIONS);
093
094    /** The value of the <code>action</code> attribute. */
095    private String m_action;
096
097    /** The value of the <code>currentuser</code> attribute. */
098    private boolean m_currentuser;
099
100    /** The value of the <code>file</code> attribute. */
101    private String m_file;
102
103    /** The value of the <code>group</code> attribute. */
104    private String m_group;
105
106    /** The value of the <code>includegroups</code> attribute. */
107    private boolean m_includegroups;
108
109    /** The value of the <code>online</code> attribute. */
110    private boolean m_online;
111
112    /** The value of the <code>subfolder</code> attribute. */
113    private boolean m_subfolder;
114
115    /** The value of the <code>user</code> attribute. */
116    private String m_user;
117
118    /**
119     * Tracks an OpenCms file according to the parameters.<p>
120     *
121     * @param action the action that should be performed
122     * @param fileName the file name to track
123     * @param subFolder flag indicating if sub folders should be included
124     * @param currentUser flag indicating if the current user should be used for the tracking action
125     * @param userName the user name that should be used for the action
126     * @param includeGroups flag indicating if the given users groups should be included
127     * @param groupName the group name that should be used for the action
128     * @param req the current request
129     *
130     * @return the result of the action, usually empty except for the check actions
131     *
132     * @throws JspException in case something goes wrong
133     */
134    public static String userTrackingTagAction(
135        String action,
136        String fileName,
137        boolean subFolder,
138        boolean currentUser,
139        String userName,
140        boolean includeGroups,
141        String groupName,
142        HttpServletRequest req) throws JspException {
143
144        String result = "";
145
146        CmsFlexController controller = CmsFlexController.getController(req);
147        CmsObject cms = controller.getCmsObject();
148
149        CmsUser user = null;
150        CmsGroup group = null;
151
152        int actionIndex = TRACK_ACTIONS_LIST.indexOf(action);
153
154        try {
155            // determine the group for the action
156            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(groupName)) {
157                group = cms.readGroup(groupName);
158            }
159            // determine the user for the action
160            if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(userName)) {
161                user = cms.readUser(userName);
162            } else if (currentUser) {
163                user = cms.getRequestContext().getCurrentUser();
164            }
165            if ((group == null) && (user == null) && (actionIndex != 4)) {
166                // set current user for the action except for check subscriptions
167                user = cms.getRequestContext().getCurrentUser();
168            }
169            // determine the file to track
170            if (CmsStringUtil.isEmptyOrWhitespaceOnly(fileName)) {
171                fileName = cms.getRequestContext().getUri();
172            }
173
174            switch (actionIndex) {
175                case 1: // subscribe
176                    if (group != null) {
177                        OpenCms.getSubscriptionManager().subscribeResourceFor(cms, group, fileName);
178                    }
179                    if (user != null) {
180                        OpenCms.getSubscriptionManager().subscribeResourceFor(cms, user, fileName);
181                    }
182                    removeSessionAttributes(new String[] {SESSION_PREFIX_SUBSCRIBED}, req);
183                    break;
184                case 2: // unsubscribe
185                    if (group != null) {
186                        OpenCms.getSubscriptionManager().unsubscribeResourceFor(cms, group, fileName);
187                    }
188                    if (user != null) {
189                        OpenCms.getSubscriptionManager().unsubscribeResourceFor(cms, user, fileName);
190                    }
191                    removeSessionAttributes(new String[] {SESSION_PREFIX_SUBSCRIBED}, req);
192                    break;
193                case 3: // checkvisited
194                    result = String.valueOf(isResourceVisited(cms, fileName, subFolder, user, req));
195                    break;
196                case 4: // checksubscribed
197                    List<CmsGroup> groups = new ArrayList<CmsGroup>();
198                    if ((group == null) && includeGroups) {
199                        if (user != null) {
200                            groups.addAll(cms.getGroupsOfUser(user.getName(), false));
201                        } else {
202                            groups.addAll(
203                                cms.getGroupsOfUser(cms.getRequestContext().getCurrentUser().getName(), false));
204                        }
205                    } else if (group != null) {
206                        groups.add(group);
207                    }
208                    result = String.valueOf(isResourceSubscribed(cms, fileName, subFolder, user, groups, req));
209                    break;
210                case 0: // visit
211                default: // default action is visit
212                    OpenCms.getSubscriptionManager().markResourceAsVisitedBy(cms, fileName, user);
213                    removeSessionAttributes(new String[] {SESSION_PREFIX_SUBSCRIBED, SESSION_PREFIX_VISITED}, req);
214            }
215        } catch (CmsException e) {
216            // store original Exception in controller in order to display it later
217            Throwable t = controller.setThrowable(e, cms.getRequestContext().getUri());
218            throw new JspException(t);
219        }
220
221        return result;
222    }
223
224    /**
225     * Returns a unique session key depending on the values of the given parameters.<p>
226     *
227     * @param prefix the key prefix to use
228     * @param fileName the file name to track
229     * @param subFolder flag indicating if sub folders should be included
230     * @param user the user that should be used
231     * @param groups the groups that should be used
232     *
233     * @return a unique session key
234     */
235    protected static String generateSessionKey(
236        String prefix,
237        String fileName,
238        boolean subFolder,
239        CmsUser user,
240        List<CmsGroup> groups) {
241
242        StringBuffer result = new StringBuffer(256);
243        result.append(prefix);
244        result.append(CmsResource.getFolderPath(fileName).hashCode()).append("_");
245        result.append(subFolder);
246        if (user != null) {
247            // add user to session key
248            result.append("_").append(user.getName().hashCode());
249        }
250        if ((groups != null) && !groups.isEmpty()) {
251            // add group(s) to session key
252            StringBuffer groupNames = new StringBuffer(128);
253            for (Iterator<CmsGroup> i = groups.iterator(); i.hasNext();) {
254                groupNames.append(i.next().getName());
255            }
256            result.append("_").append(groupNames.toString().hashCode());
257        }
258        return result.toString();
259    }
260
261    /**
262     * Returns if the given resource is subscribed to the user or groups.<p>
263     *
264     * @param cms the current users context
265     * @param fileName the file name to track
266     * @param subFolder flag indicating if sub folders should be included
267     * @param user the user that should be used for the check
268     * @param groups the groups that should be used for the check
269     * @param req the current request
270     *
271     * @return <code>true</code> if the given resource is subscribed to the user or groups, otherwise <code>false</code>
272     *
273     * @throws CmsException if something goes wrong
274     */
275    protected static boolean isResourceSubscribed(
276        CmsObject cms,
277        String fileName,
278        boolean subFolder,
279        CmsUser user,
280        List<CmsGroup> groups,
281        HttpServletRequest req) throws CmsException {
282
283        CmsResource checkResource = cms.readResource(fileName);
284
285        HttpSession session = req.getSession(true);
286        String sessionKey = generateSessionKey(SESSION_PREFIX_SUBSCRIBED, fileName, subFolder, user, groups);
287        // try to get the subscribed resources from a session attribute
288        @SuppressWarnings("unchecked")
289        List<CmsResource> subscribedResources = (List<CmsResource>)session.getAttribute(sessionKey);
290        if (subscribedResources == null) {
291            // first call, read subscribed resources and store them to session attribute
292            CmsSubscriptionFilter filter = new CmsSubscriptionFilter();
293            filter.setParentPath(CmsResource.getFolderPath(checkResource.getRootPath()));
294            filter.setIncludeSubfolders(subFolder);
295            filter.setUser(user);
296            filter.setGroups(groups);
297            filter.setMode(CmsSubscriptionReadMode.ALL);
298            subscribedResources = OpenCms.getSubscriptionManager().readSubscribedResources(cms, filter);
299            session.setAttribute(sessionKey, subscribedResources);
300        }
301        return subscribedResources.contains(checkResource);
302    }
303
304    /**
305     * Returns if the given resource was visited by the user.<p>
306     *
307     * @param cms the current users context
308     * @param fileName the file name to track
309     * @param subFolder flag indicating if sub folders should be included
310     * @param user the user that should be used for the check
311     * @param req the current request
312     *
313     * @return <code>true</code> if the given resource was visited by the user, otherwise <code>false</code>
314     *
315     * @throws CmsException if something goes wrong
316     */
317    protected static boolean isResourceVisited(
318        CmsObject cms,
319        String fileName,
320        boolean subFolder,
321        CmsUser user,
322        HttpServletRequest req) throws CmsException {
323
324        CmsResource checkResource = cms.readResource(fileName);
325
326        HttpSession session = req.getSession(true);
327        String sessionKey = generateSessionKey(SESSION_PREFIX_VISITED, fileName, subFolder, user, null);
328        // try to get the visited resources from a session attribute
329        @SuppressWarnings("unchecked")
330        List<CmsResource> visitedResources = (List<CmsResource>)req.getSession(true).getAttribute(sessionKey);
331        if (visitedResources == null) {
332            // first call, read visited resources and store them to session attribute
333            CmsVisitedByFilter filter = new CmsVisitedByFilter();
334            filter.setUser(user);
335            filter.setParentPath(CmsResource.getFolderPath(checkResource.getRootPath()));
336            filter.setIncludeSubfolders(subFolder);
337            visitedResources = OpenCms.getSubscriptionManager().readResourcesVisitedBy(cms, filter);
338            session.setAttribute(sessionKey, visitedResources);
339        }
340
341        return visitedResources.contains(checkResource);
342    }
343
344    /**
345     * Removes all session attributes starting with the given prefixes.<p>
346     *
347     * @param prefixes the prefixes of the session attributes to remove
348     * @param req the current request
349     */
350    protected static void removeSessionAttributes(String[] prefixes, HttpServletRequest req) {
351
352        HttpSession session = req.getSession(true);
353        @SuppressWarnings("unchecked")
354        Enumeration<String> en = session.getAttributeNames();
355        while (en.hasMoreElements()) {
356            String attrKey = en.nextElement();
357            for (int i = 0; i < prefixes.length; i++) {
358                if (attrKey.startsWith(prefixes[i])) {
359                    session.removeAttribute(attrKey);
360                }
361            }
362        }
363    }
364
365    /**
366     * @see javax.servlet.jsp.tagext.Tag#doStartTag()
367     */
368    @Override
369    public int doStartTag() throws JspException {
370
371        ServletRequest req = pageContext.getRequest();
372
373        // this will always be true if the page is called through OpenCms
374        if (CmsFlexController.isCmsRequest(req)) {
375
376            CmsObject cms = CmsFlexController.getCmsObject(req);
377
378            if (m_online && !cms.getRequestContext().getCurrentProject().isOnlineProject()) {
379                // the online flag is set and we are in an offline project, do not track file
380                return SKIP_BODY;
381            }
382            try {
383                String result = userTrackingTagAction(
384                    m_action,
385                    m_file,
386                    m_subfolder,
387                    m_currentuser,
388                    m_user,
389                    m_includegroups,
390                    m_group,
391                    (HttpServletRequest)req);
392                pageContext.getOut().print(result);
393            } catch (Exception ex) {
394                if (LOG.isErrorEnabled()) {
395                    LOG.error(Messages.get().getBundle().key(Messages.ERR_PROCESS_TAG_1, "usertracking"), ex);
396                }
397                throw new JspException(ex);
398            }
399        }
400        return SKIP_BODY;
401    }
402
403    /**
404     * Returns the action that should be performed, i.e. mark as visited or subscribe/unsubscribe.<p>
405     *
406     * @return the action that should be performed
407     */
408    public String getAction() {
409
410        return m_action != null ? m_action : "";
411    }
412
413    /**
414     * Returns the current user flag.<p>
415     *
416     * @return the current user flag
417     */
418    public String getCurrentuser() {
419
420        return String.valueOf(m_currentuser);
421    }
422
423    /**
424     * Returns the file name to track.<p>
425     *
426     * @return the file name to track
427     */
428    public String getFile() {
429
430        return m_file != null ? m_file : "";
431    }
432
433    /**
434     * Returns the group name that is used for the tracking.<p>
435     *
436     * @return the group name that is used for the tracking
437     */
438    public String getGroup() {
439
440        return m_group != null ? m_group : "";
441    }
442
443    /**
444     * Returns the include groups flag.<p>
445     *
446     * @return the include groups flag
447     */
448    public String getIncludegroups() {
449
450        return String.valueOf(m_includegroups);
451    }
452
453    /**
454     * Returns the online flag.<p>
455     *
456     * @return the online flag
457     */
458    public String getOnline() {
459
460        return String.valueOf(m_online);
461    }
462
463    /**
464     * Returns the subfolder flag.<p>
465     *
466     * @return the subfolder flag
467     */
468    public String getSubfolder() {
469
470        return String.valueOf(m_subfolder);
471    }
472
473    /**
474     * Returns the user name that is used for the tracking.<p>
475     *
476     * @return the user name that is used for the tracking
477     */
478    public String getUser() {
479
480        return m_user != null ? m_user : "";
481    }
482
483    /**
484     * @see javax.servlet.jsp.tagext.Tag#release()
485     */
486    @Override
487    public void release() {
488
489        super.release();
490        m_action = null;
491        m_file = null;
492        m_group = null;
493        m_includegroups = false;
494        m_online = false;
495        m_subfolder = false;
496        m_user = null;
497    }
498
499    /**
500     * Sets the action that should be performed, i.e. mark as visited or subscribe/unsubscribe.<p>
501     *
502     * @param action the action that should be performed
503     */
504    public void setAction(String action) {
505
506        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(action)) {
507            m_action = action;
508        }
509    }
510
511    /**
512     * Sets the current user flag.<p>
513     *
514     * Current user is <code>false</code> by default.<p>
515     *
516     * @param currentUser the flag to set
517     */
518    public void setCurrentuser(String currentUser) {
519
520        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(currentUser)) {
521            m_currentuser = Boolean.valueOf(currentUser).booleanValue();
522        }
523    }
524
525    /**
526     * Sets the file name to track.<p>
527     *
528     * @param file the file name to track
529     */
530    public void setFile(String file) {
531
532        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(file)) {
533            m_file = file;
534        }
535    }
536
537    /**
538     * Sets the group name that is used for the tracking.<p>
539     *
540     * @param group the group name that is used for the tracking
541     */
542    public void setGroup(String group) {
543
544        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(group)) {
545            m_group = group;
546        }
547    }
548
549    /**
550     * Sets the include groups flag.<p>
551     *
552     * Include groups is <code>false</code> by default.<p>
553     *
554     * @param includeGroups the flag to set
555     */
556    public void setIncludegroups(String includeGroups) {
557
558        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(includeGroups)) {
559            m_includegroups = Boolean.valueOf(includeGroups).booleanValue();
560        }
561    }
562
563    /**
564     * Sets the online flag.<p>
565     *
566     * Online is <code>false</code> by default.<p>
567     *
568     * @param online the flag to set
569     */
570    public void setOnline(String online) {
571
572        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(online)) {
573            m_online = Boolean.valueOf(online).booleanValue();
574        }
575    }
576
577    /**
578     * Sets the subfolder flag.<p>
579     *
580     * Online is <code>false</code> by default.<p>
581     *
582     * @param subfolder the flag to set
583     */
584    public void setSubfolder(String subfolder) {
585
586        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(subfolder)) {
587            m_subfolder = Boolean.valueOf(subfolder).booleanValue();
588        }
589    }
590
591    /**
592     * Sets the user name that is used for the tracking.<p>
593     *
594     * @param user the user name that is used for the tracking
595     */
596    public void setUser(String user) {
597
598        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(user)) {
599            m_user = user;
600        }
601    }
602
603}