///////////////////////////////////////////////////////////////////////////// // // Project ProjectForge Community Edition // www.projectforge.org // // Copyright (C) 2001-2014 Kai Reinhard (k.reinhard@micromata.de) // // ProjectForge is dual-licensed. // // This community edition is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 3 of the License. // // This community edition is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General // Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, see http://www.gnu.org/licenses/. // ///////////////////////////////////////////////////////////////////////////// package org.projectforge.web.calendar; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.TimeZone; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.Description; import net.fortuna.ical4j.model.property.Location; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.Version; import net.ftlines.wicket.fullcalendar.Event; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.lang.StringUtils; import org.apache.log4j.MDC; import org.joda.time.DateTime; import org.projectforge.access.AccessException; import org.projectforge.calendar.DayHolder; import org.projectforge.calendar.ICal4JUtils; import org.projectforge.common.NumberHelper; import org.projectforge.common.StringHelper; import org.projectforge.plugins.teamcal.TeamCalConfig; import org.projectforge.registry.Registry; import org.projectforge.timesheet.TimesheetDO; import org.projectforge.timesheet.TimesheetDao; import org.projectforge.timesheet.TimesheetFilter; import org.projectforge.user.PFUserContext; import org.projectforge.user.PFUserDO; import org.projectforge.user.ProjectForgeGroup; import org.projectforge.user.UserDao; import org.projectforge.user.UserRights; import org.projectforge.web.WebConfiguration; import org.projectforge.web.timesheet.TimesheetEventsProvider; /** * Feed Servlet, which generates a 'text/calendar' output of the last four mounts. Currently relevant informations are date, start- and stop * time and last but not least the location of an event. * * @author Kai Reinhard (k.reinhard@micromata.de) * */ public class CalendarFeed extends HttpServlet { private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(CalendarFeed.class); private static final long serialVersionUID = 1480433876190009435L; private static final int PERIOD_IN_MONTHS = 4; private static final String PARAM_NAME_TIMESHEET_USER = "timesheetUser"; private static final String PARAM_NAME_HOLIDAYS = "holidays"; private static final String PARAM_NAME_WEEK_OF_YEARS = "weekOfYears"; private static final List<CalendarFeedHook> feedHooks = new LinkedList<CalendarFeedHook>(); /** * setup event is needed for empty calendars */ public static final String SETUP_EVENT = "SETUP EVENT"; public static String getUrl() { return getUrl(null); } /** * @return The url for downloading timesheets (including context), e. g. /ProjectForge/export/ProjectForge.ics?user=.... */ public static String getUrl4Timesheets(final Integer timesheetUserId) { return getUrl("&" + PARAM_NAME_TIMESHEET_USER + "=" + timesheetUserId); } /** * @return The url for downloading timesheets (including context), e. g. /ProjectForge/export/ProjectForge.ics?user=.... */ public static String getUrl4Holidays() { return getUrl("&" + PARAM_NAME_HOLIDAYS + "=true"); } /** * @return The url for downloading timesheets (including context), e. g. /ProjectForge/export/ProjectForge.ics?user=.... */ public static String getUrl4WeekOfYears() { return getUrl("&" + PARAM_NAME_WEEK_OF_YEARS + "=true"); } /** * @param additionalParams Request parameters such as "&calId=42", may be null. * @return The url for downloading calendars (without context), e. g. /export/ProjectForge.ics?user=... */ public static String getUrl(final String additionalParams) { final PFUserDO user = PFUserContext.getUser(); final UserDao userDao = Registry.instance().getDao(UserDao.class); final String authenticationKey = userDao.getAuthenticationToken(user.getId()); final StringBuffer buf = new StringBuffer(); buf.append("token=").append(authenticationKey); if (additionalParams != null) { buf.append(additionalParams); } final String encryptedParams = Registry.instance().getDao(UserDao.class).encrypt(buf.toString()); final String result = "/export/ProjectForge.ics?user=" + user.getId() + "&q=" + encryptedParams; return result; } @Override protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { if (WebConfiguration.isUpAndRunning() == false) { log.error("System isn't up and running, CalendarFeed call denied. The system is may-be in start-up phase or in maintenance mode."); resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); return; } PFUserDO user = null; String logMessage = null; try { MDC.put("ip", req.getRemoteAddr()); MDC.put("session", req.getSession().getId()); if (StringUtils.isBlank(req.getParameter("user")) || StringUtils.isBlank(req.getParameter("q"))) { resp.sendError(HttpStatus.SC_BAD_REQUEST); log.error("Bad request, parameters user and q not given. Query string is: " + req.getQueryString()); return; } final String encryptedParams = req.getParameter("q"); final Integer userId = NumberHelper.parseInteger(req.getParameter("user")); if (userId == null) { log.error("Bad request, parameter user is not an integer: " + req.getQueryString()); return; } final Registry registry = Registry.instance(); user = registry.getUserGroupCache().getUser(userId); if (user == null) { log.error("Bad request, user not found: " + req.getQueryString()); return; } PFUserContext.setUser(user); MDC.put("user", user.getUsername()); final String decryptedParams = registry.getDao(UserDao.class).decrypt(userId, encryptedParams); if (decryptedParams == null) { log.error("Bad request, can't decrypt parameter q (may-be the user's authentication token was changed): " + req.getQueryString()); return; } final Map<String, String> params = StringHelper.getKeyValues(decryptedParams, "&"); final Calendar calendar = createCal(params, userId, params.get("token"), params.get(PARAM_NAME_TIMESHEET_USER)); final StringBuffer buf = new StringBuffer(); boolean first = true; for (final Map.Entry<String, String> entry : params.entrySet()) { if ("token".equals(entry.getKey()) == true) { continue; } first = StringHelper.append(buf, first, entry.getKey(), ", "); buf.append("=").append(entry.getValue()); } logMessage = buf.toString(); log.info("Getting calendar entries for: " + logMessage); if (calendar == null) { resp.sendError(HttpStatus.SC_BAD_REQUEST); log.error("Bad request, can't find calendar."); return; } resp.setContentType("text/calendar"); final CalendarOutputter output = new CalendarOutputter(false); try { output.output(calendar, resp.getOutputStream()); } catch (final ValidationException ex) { ex.printStackTrace(); } } finally { log.info("Finished request: " + logMessage); PFUserContext.setUser(null); MDC.remove("ip"); MDC.remove("session"); if (user != null) { MDC.remove("user"); } } } /** * creates a calendar for the user, identified by his name and authentication key. * @param params * * @param userName * @param userKey * @return a calendar, null if authentication fails */ private Calendar createCal(final Map<String, String> params, final Integer userId, final String authKey, final String timesheetUserParam) { final UserDao userDao = Registry.instance().getDao(UserDao.class); final PFUserDO loggedInUser = userDao.getUserByAuthenticationToken(userId, authKey); if (loggedInUser == null) { return null; } PFUserDO timesheetUser = null; if (StringUtils.isNotBlank(timesheetUserParam) == true) { final Integer timesheetUserId = NumberHelper.parseInteger(timesheetUserParam); if (timesheetUserId != null) { if (timesheetUserId.equals(loggedInUser.getId()) == false) { log.error("Not yet allowed: all users are only allowed to download their own time-sheets."); return null; } timesheetUser = userDao.getUserGroupCache().getUser(timesheetUserId); if (timesheetUser == null) { log.error("Time-sheet user with id '" + timesheetUserParam + "' not found."); return null; } } } // creating a new calendar final Calendar calendar = new Calendar(); final Locale locale = PFUserContext.getLocale(); calendar.getProperties().add( new ProdId("-//" + loggedInUser.getDisplayUsername() + "//ProjectForge//" + locale.toString().toUpperCase())); calendar.getProperties().add(Version.VERSION_2_0); calendar.getProperties().add(CalScale.GREGORIAN); // setup event is needed for empty calendars calendar.getComponents().add(new VEvent(new net.fortuna.ical4j.model.Date(0), SETUP_EVENT)); // adding events for (final VEvent event : getEvents(params, timesheetUser)) { calendar.getComponents().add(event); } return calendar; } /** * builds the list of events * * @return */ private List<VEvent> getEvents(final Map<String, String> params, PFUserDO timesheetUser) { final PFUserDO loggedInUser = PFUserContext.getUser(); if (loggedInUser == null) { throw new AccessException("No logged-in-user found!"); } final List<VEvent> events = new ArrayList<VEvent>(); final TimeZone timezone = ICal4JUtils.getUserTimeZone(); final java.util.Calendar cal = java.util.Calendar.getInstance(PFUserContext.getTimeZone()); boolean eventsExist = false; for (final CalendarFeedHook hook : feedHooks) { final List<VEvent> list = hook.getEvents(params, timezone); if (list != null && list.size() > 0) { events.addAll(list); eventsExist = true; } } if (timesheetUser != null) { if (loggedInUser.getId().equals(timesheetUser.getId()) == false && isOtherUsersAllowed() == false) { // Only project managers, controllers and administrative staff is allowed to subscribe time-sheets of other users. log.warn("User tried to get time-sheets of other user: " + timesheetUser); timesheetUser = loggedInUser; } // initializes timesheet filter final TimesheetFilter filter = new TimesheetFilter(); filter.setUserId(timesheetUser.getId()); filter.setDeleted(false); filter.setStopTime(cal.getTime()); // calculates the offset of the calendar final int offset = cal.get(java.util.Calendar.MONTH) - PERIOD_IN_MONTHS; if (offset < 0) { setCalDate(cal, cal.get(java.util.Calendar.YEAR) - 1, 12 + offset); } else { setCalDate(cal, cal.get(java.util.Calendar.YEAR), offset); } filter.setStartTime(cal.getTime()); final TimesheetDao timesheetDao = Registry.instance().getDao(TimesheetDao.class); final List<TimesheetDO> timesheetList = timesheetDao.getList(filter); // iterate over all timesheets and adds each event to the calendar for (final TimesheetDO timesheet : timesheetList) { final String uid = TeamCalConfig.get().createTimesheetUid(timesheet.getId()); String summary; if (eventsExist == true) { summary = TimesheetEventsProvider.getTitle(timesheet) + " (ts)"; } else { summary = TimesheetEventsProvider.getTitle(timesheet); } final VEvent vEvent = ICal4JUtils.createVEvent(timesheet.getStartTime(), timesheet.getStopTime(), uid, summary); if (StringUtils.isNotBlank(timesheet.getDescription()) == true) { vEvent.getProperties().add(new Description(timesheet.getDescription())); } if (StringUtils.isNotBlank(timesheet.getLocation()) == true) { vEvent.getProperties().add(new Location(timesheet.getLocation())); } events.add(vEvent); } } final String holidays = params.get(PARAM_NAME_HOLIDAYS); if ("true".equals(holidays) == true) { final HolidayEventsProvider holidaysEventsProvider = new HolidayEventsProvider(); DateTime holidaysFrom = new DateTime(PFUserContext.getDateTimeZone()); holidaysFrom = holidaysFrom.dayOfYear().withMinimumValue().millisOfDay().withMinimumValue().minusYears(2); final DateTime holidayTo = holidaysFrom.plusYears(6); for (final Event event : holidaysEventsProvider.getEvents(holidaysFrom, holidayTo)) { final Date fromDate = event.getStart().toDate(); final Date toDate = event.getEnd() != null ? event.getEnd().toDate() : fromDate; final VEvent vEvent = ICal4JUtils.createVEvent(fromDate, toDate, "pf-holiday" + event.getId(), event.getTitle(), true); events.add(vEvent); } } final String weeksOfYear = params.get(PARAM_NAME_WEEK_OF_YEARS); if ("true".equals(weeksOfYear) == true) { final DayHolder from = new DayHolder(); from.setBeginOfYear().add(java.util.Calendar.YEAR, -2).setBeginOfWeek(); final DayHolder to = new DayHolder(from); to.add(java.util.Calendar.YEAR, 6); final DayHolder current = new DayHolder(from); int paranoiaCounter = 0; do { final VEvent vEvent = ICal4JUtils.createVEvent(current.getDate(), current.getDate(), "pf-weekOfYear" + current.getYear() + "-" + paranoiaCounter, PFUserContext.getLocalizedString("calendar.weekOfYearShortLabel") + " " + current.getWeekOfYear(), true); events.add(vEvent); current.add(java.util.Calendar.WEEK_OF_YEAR, 1); if (++paranoiaCounter > 500) { log.warn("Dear developer, please have a look here, paranoiaCounter exceeded! Aborting calculation of weeks of year."); } } while (current.before(to) == true); } // Integer hrPlanningUserId = NumberHelper.parseInteger(params.get(PARAM_NAME_HR_PLANNING)); // if (hrPlanningUserId != null) { // if (loggedInUser.getId().equals(hrPlanningUserId) == false && isOtherUsersAllowed() == false) { // // Only project managers, controllers and administrative staff is allowed to subscribe time-sheets of other users. // log.warn("User tried to get time-sheets of other user: " + timesheetUser); // hrPlanningUserId = loggedInUser.getId(); // } // final HRPlanningDao hrPlanningDao = Registry.instance().getDao(HRPlanningDao.class); // final HRPlanningEventsProvider hrPlanningEventsProvider = new HRPlanningEventsProvider(new CalendarFilter().setShowPlanning(true) // .setTimesheetUserId(hrPlanningUserId), hrPlanningDao); // DateTime planningFrom = new DateTime(PFUserContext.getDateTimeZone()); // planningFrom = planningFrom.dayOfYear().withMinimumValue().millisOfDay().withMinimumValue().minusYears(1); // final DateTime planningTo = planningFrom.plusYears(4); // for (final Event event : hrPlanningEventsProvider.getEvents(planningFrom, planningTo)) { // final Date fromDate = event.getStart().toDate(); // final Date toDate = event.getEnd() != null ? event.getEnd().toDate() : fromDate; // final VEvent vEvent = ICal4JUtils.createVEvent(fromDate, toDate, "pf-hr-planning" + event.getId(), event.getTitle(), true); // events.add(vEvent); // } // } return events; } /** * sets the calendar to a special date. Used to calculate the year offset of an negative time period. When the time period is set to 4 * month and the current month is at the begin of a year, the year-number must be decremented by one * * @param cal * @param year * @param mounth */ private void setCalDate(final java.util.Calendar cal, final int year, final int mounth) { cal.clear(); cal.set(java.util.Calendar.YEAR, year); cal.set(java.util.Calendar.MONTH, mounth); } private boolean isOtherUsersAllowed() { return UserRights.getAccessChecker().isLoggedInUserMemberOfGroup(ProjectForgeGroup.FINANCE_GROUP, ProjectForgeGroup.CONTROLLING_GROUP, ProjectForgeGroup.PROJECT_MANAGER); } public static void registerFeedHook(final CalendarFeedHook hook) { feedHooks.add(hook); } }