org.jasig.portal.rest.StatsLayoutModificationsController.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.portal.rest.StatsLayoutModificationsController.java

Source

/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.jasig.portal.rest;

import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.portal.EntityIdentifier;
import org.jasig.portal.security.AdminEvaluator;
import org.jasig.portal.security.IAuthorizationPrincipal;
import org.jasig.portal.security.IPerson;
import org.jasig.portal.security.IPersonManager;
import org.jasig.portal.services.AuthorizationService;
import org.jasig.portal.utils.cache.CacheFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.simple.ParameterizedRowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/**
 * Spring controller that returns a JSON representation of how many times users 
 * have either added each portlet in the specified number of days, counting 
 * backwards from the specified day (inclusive). 
 * <p>Request parameters:</p>
 * <ul>
 *   <li>days: Number of calendar days to include in the report; default is 30</li>
 *   <li>fromDate: Date (inclusive) from which to count backwards; default is today</li>
 * </ul>
 *
 * @author Drew Wills, drew@unicon.net
 */
@Controller
public class StatsLayoutModificationsController implements InitializingBean {

    private static final int MIN_DAYS = 0;

    private DataSource dataSource;
    private EventCountFactory factory;
    private int maxDays = 365; // default
    private final Pattern datePattern = Pattern.compile("\\d+/\\d+/\\d+");
    private final DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
    private IPersonManager personManager;
    private CacheFactory cacheFactory;
    private final Log log = LogFactory.getLog(getClass());

    @Resource(name = "RawEventsDB")
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void setMaxDays(int maxDays) {
        this.maxDays = maxDays;
    }

    @Autowired
    public void setPersonManager(IPersonManager personManager) {
        this.personManager = personManager;
    }

    @Autowired
    public void setCacheFactory(CacheFactory cacheFactory) {
        this.cacheFactory = cacheFactory;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.factory = new EventCountFactory(dataSource, cacheFactory);
    }

    @RequestMapping(value = "/userLayoutModificationsCounts")
    public ModelAndView getEventCounts(HttpServletRequest req, HttpServletResponse res) throws ServletException {

        // Days parameter
        int days = 30; // default
        if (req.getParameter("days") != null) {
            String daysParam = req.getParameter("days").trim();
            try {
                days = Integer.parseInt(daysParam);
            } catch (NumberFormatException nfe) {
                String msg = "Unrecognizable days parameter (must be a valid integer): " + daysParam;
                log.warn(msg, nfe);
                throw new ServletException(msg, nfe);
            }
        }

        // fromDate parameter
        Calendar dateOnOrBefore = Calendar.getInstance();
        dateOnOrBefore.set(Calendar.HOUR_OF_DAY, 0);
        dateOnOrBefore.set(Calendar.MINUTE, 0);
        dateOnOrBefore.set(Calendar.SECOND, 0);
        dateOnOrBefore.set(Calendar.MILLISECOND, 0);
        dateOnOrBefore.roll(Calendar.DATE, true); // default is tomorrow with time fields cleared
        if (req.getParameter("fromDate") != null) {
            String fromDateParam = req.getParameter("fromDate").trim();
            // If the user doesn't enter a date, the UI sends "today" (or other 
            // string), so ignore anything that's not even close...
            if (datePattern.matcher(fromDateParam).matches()) {
                try {
                    Date fromDate = format.parse(fromDateParam);
                    // Need to add one day, since the report is inclusive
                    Calendar cal = Calendar.getInstance();
                    cal.setTime(fromDate);
                    cal.add(Calendar.DATE, 1);
                    dateOnOrBefore = cal;
                } catch (ParseException pe) {
                    // Passing a bad date is ok, it just results in the default
                    if (log.isInfoEnabled()) {
                        String msg = "Unrecognizable fromDate parameter (format 'mm/dd/yyyy'): " + fromDateParam;
                        log.info(msg, pe);
                    }
                }
            }
        }

        // Be certain days is within prescribed limits
        if (days < MIN_DAYS) {
            days = MIN_DAYS;
        } else if (days > maxDays) {
            days = maxDays;
        }

        List<CountingTuple> completeList = factory.getEventCounts(dateOnOrBefore.getTime(), days);

        IPerson user = personManager.getPerson(req);
        List<CountingTuple> filteredList = filterByPermissions(user, completeList);

        return new ModelAndView("jsonView", "counts", filteredList);

    }

    /*
     * Implementation
     */

    private List<CountingTuple> filterByPermissions(IPerson user, List<CountingTuple> completeList) {

        // Assertions
        if (user == null) {
            String msg = "Argument 'user' cannot be null";
            throw new IllegalArgumentException(msg);
        }
        if (completeList == null) {
            String msg = "Argument 'completeList' cannot be null";
            throw new IllegalArgumentException(msg);
        }

        if (AdminEvaluator.isAdmin(user)) {
            // Admins may see the complete list
            return completeList;
        }

        EntityIdentifier ei = user.getEntityIdentifier();
        IAuthorizationPrincipal ap = AuthorizationService.instance().newPrincipal(ei.getKey(), ei.getType());

        List<CountingTuple> rslt = new ArrayList<CountingTuple>();
        for (CountingTuple tuple : completeList) {
            if (ap.canSubscribe(String.valueOf(tuple.getId()))) {
                rslt.add(tuple);
            }
        }

        return rslt;

    }

    /*
     * Nested Types
     */

    public static final class CountingTuple implements Comparable<CountingTuple> {

        private final int id;
        private final String portletFName;
        private final String portletTitle;
        private String portletDescription = "[no description available]"; // default
        private final int count;

        public CountingTuple(int id, String portletFName, String portletTitle, String portletDescription,
                int count) {

            // Assertions
            if (portletFName == null) {
                String msg = "Argument 'portletFName' cannot be null";
                throw new IllegalArgumentException(msg);
            }
            if (portletTitle == null) {
                String msg = "Argument 'portletTitle' cannot be null";
                throw new IllegalArgumentException(msg);
            }
            // NB:  'portletDescription' actually can be null

            this.id = id;
            this.portletFName = portletFName;
            this.portletTitle = portletTitle;
            if (portletDescription != null) {
                this.portletDescription = portletDescription;
            }
            this.count = count;
        }

        public int getId() {
            return id;
        }

        public String getPortletFName() {
            return portletFName;
        }

        public String getPortletTitle() {
            return portletTitle;
        }

        public String getPortletDescription() {
            return portletDescription;
        }

        public int getCount() {
            return count;
        }

        @Override
        public int compareTo(CountingTuple tuple) {
            // Natural order for these is count
            return new Integer(count).compareTo(tuple.getCount());
        }

    }

    private static final class EventCountFactory {

        private static final String ADDED_PORTLETS_SQL = "SELECT uppd.portlet_def_id, uppd.portlet_fname, uppd.portlet_title, uppd.portlet_desc, (SELECT COUNT(*) "
                + "FROM stats_event ste, stats_event_type stet, stats_channel stc " + "WHERE ste.type_id = stet.id "
                + "AND stet.type = 'LAYOUT_CHANNEL_ADDED' " + "AND ste.id = stc.event_id "
                + "AND stc.definition_id = uppd.portlet_def_id " + "AND ste.act_date >= ? AND ste.act_date <= ?) "
                + "FROM up_portlet_def uppd";
        private static final String PORTLETS_ADDED_CACHE_KEY = "PortalStats.org.jasig.portal.rest.StatsLayoutModificationsController.portletsAdded";

        private final SimpleJdbcTemplate simpleJdbcTemplate;
        private final RowMapperImpl rowMapper = new RowMapperImpl();
        private final Map<CacheTuple, List<CountingTuple>> cache;

        public EventCountFactory(DataSource dataSource, CacheFactory cacheFactory) {

            // Assertions
            if (dataSource == null) {
                String msg = "Argument 'dataSource' cannot be null";
                throw new IllegalArgumentException(msg);
            }
            if (cacheFactory == null) {
                String msg = "Argument 'cacheFactory' cannot be null";
                throw new IllegalArgumentException(msg);
            }

            this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
            this.cache = cacheFactory.getCache(PORTLETS_ADDED_CACHE_KEY);

        }

        public List<CountingTuple> getEventCounts(Date dateOnOrBefore, int days) {
            CacheTuple tuple = new CacheTuple(dateOnOrBefore.getTime(), days);
            List<CountingTuple> rslt = (List<CountingTuple>) cache.get(tuple);
            if (rslt == null) {
                rslt = buildEventCounts(dateOnOrBefore, days);
                cache.put(tuple, rslt);
            }
            return rslt;
        }

        private List<CountingTuple> buildEventCounts(Date dateOnOrBefore, int days) {

            Calendar cal = Calendar.getInstance();
            cal.add(Calendar.DATE, -days);
            Date dateOnOrAfter = cal.getTime();

            List<CountingTuple> rslt = simpleJdbcTemplate.query(ADDED_PORTLETS_SQL, rowMapper, dateOnOrAfter,
                    dateOnOrBefore);
            // Remove rows with zero count... not interested in those
            int index = 0;
            while (rslt.size() > index) {
                if (rslt.get(index).getCount() == 0) {
                    rslt.remove(index);
                } else {
                    index += 1;
                }
            }
            // We're interested in descending order 
            Collections.sort(rslt, Collections.reverseOrder());

            return rslt;

        }

    }

    private static final class RowMapperImpl implements ParameterizedRowMapper<CountingTuple> {

        @Override
        public CountingTuple mapRow(ResultSet rs, int index) throws SQLException {
            return new CountingTuple(rs.getInt("portlet_def_id"), rs.getString("portlet_fname"),
                    rs.getString("portlet_title"), rs.getString("portlet_desc"), rs.getInt(5));
        }

    }

    private static final class CacheTuple implements Serializable {

        private static final long serialVersionUID = 1L;

        private final long time;
        private final int days;

        public CacheTuple(long time, int days) {
            this.time = time;
            this.days = days;
        }

        @Override
        public boolean equals(Object o) {
            boolean rslt = false;
            if (o instanceof CacheTuple) {
                CacheTuple p = (CacheTuple) o;
                rslt = p.time == time && p.days == days;
            }
            return rslt;
        }

    }

}