1    /*
2     * Copyright 2009 :torweg free software group
3     *
4     * This program is free software: you can redistribute it and/or modify
5     * it under the terms of the GNU General Public License as published by
6     * the Free Software Foundation, either version 3 of the License, or
7     * (at your option) any later version.
8     * 
9     * This program is distributed in the hope that it will be useful,
10    * but WITHOUT ANY WARRANTY; without even the implied warranty of
11    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    * GNU General Public License for more details.
13    * 
14    * You should have received a copy of the GNU General Public License
15    * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16    *
17    */
18   package org.torweg.pulse.component.statistics.view;
19   
20   import java.io.File;
21   import java.text.SimpleDateFormat;
22   import java.util.Date;
23   import java.util.List;
24   import java.util.Locale;
25   import java.util.Map;
26   
27   import org.hibernate.Session;
28   import org.hibernate.Transaction;
29   import org.jfree.data.time.Day;
30   import org.jfree.data.time.Hour;
31   import org.jfree.data.time.Month;
32   import org.jfree.data.time.RegularTimePeriod;
33   import org.jfree.data.time.Week;
34   import org.jfree.data.time.Year;
35   import org.torweg.pulse.component.statistics.model.StatisticsServer;
36   import org.torweg.pulse.component.statistics.model.aggregation.AbstractAggregation;
37   import org.torweg.pulse.invocation.lifecycle.Lifecycle;
38   import org.torweg.pulse.service.PulseException;
39   import org.torweg.pulse.service.event.ForbiddenEvent;
40   import org.torweg.pulse.service.event.PDFOutputEvent;
41   import org.torweg.pulse.service.event.XSLTOutputEvent;
42   import org.torweg.pulse.service.event.Event.Disposition;
43   import org.torweg.pulse.service.request.Command;
44   import org.torweg.pulse.service.request.Parameter;
45   import org.torweg.pulse.service.request.ServiceRequest;
46   import org.torweg.pulse.service.request.ServiceSession;
47   import org.torweg.pulse.util.time.Duration;
48   import org.torweg.pulse.util.time.Period;
49   
50   /**
51    * Abstract base-class to derive a "view" for statistical data from.
52    * 
53    * @param <T>
54    *            the actual implementation of:
55    *            {@code AbstractStatisticsViewControllerConfiguration}
56    * @param <U>
57    *            the actual implementation of:
58    *            {@code AbstractStatisticsViewControllerResultData}
59    * @param <V>
60    *            the actual implementation of:
61    *            {@code AbstractStatisticsViewControllerResult} as used for the
62    *            initialisation of the view
63    * 
64    * @author Daniel Dietz
65    * @version $Revision: 1832 $
66    */
67   public abstract class AbstractStatisticsViewController<T extends AbstractStatisticsViewControllerConfiguration, U extends AbstractStatisticsViewControllerResultData, V extends AbstractStatisticsViewControllerResult<U>>
68           extends AbstractStatisticsController<T, V> {
69   
70       /**
71        * The default chart width - <tt>600</tt> - being used if none is specified.
72        */
73       protected static final int DEFAULT_CHART_WIDTH = 600;
74   
75       /**
76        * The default chart height - <tt>400</tt> - being used if none is
77        * specified.
78        */
79       protected static final int DEFAULT_CHART_HEIGHT = 400;
80   
81       /**
82        * The default server id - <tt>0L</tt> - being used if none is specified.
83        */
84       protected static final long NO_SERVERID = 0L;
85   
86       /**
87        * Builds the result data for the given {@code StatisticsServer} and the
88        * given {@code Duration} using the given {@code Period} as
89        * "data resolution".
90        * 
91        * @param duration
92        *            {@code Duration}
93        * @param dataResolution
94        *            {@code Period} to be used as "data resolution"
95        * @param server
96        *            {@code StatisticsServer}
97        * @param s
98        *            {@code Session}
99        * 
100       * @return {@code &lt;U&gt;}
101       */
102      protected abstract U buildViewResultData(final Duration duration,
103              final Period dataResolution, final StatisticsServer server,
104              final Session s);
105  
106      /**
107       * Factory method to retrieve an actual new result.
108       * 
109       * @param configuration
110       *            {@code &lt;T&gt;}
111       * @param data
112       *            {@code &lt;U&gt;}
113       * 
114       * @return {@code &lt;V&gt;}
115       */
116      protected abstract V newResult(T configuration, U data);
117  
118      /**
119       * Builds the {@code &lt;V&lt;U&gt;&gt;} initialising the view.
120       * 
121       * @param request
122       *            {@code ServiceRequest}
123       * @param keepDataInSession
124       *            {@code true} to keep the data in the session, {@code false}
125       *            otherwise
126       * 
127       * @return {@code &lt;V&gt;}
128       */
129      protected final V initView(final ServiceRequest request,
130              final boolean keepDataInSession) {
131  
132          Command c = request.getCommand();
133          U resultData = null;
134          Session s = Lifecycle.getHibernateDataSource().createNewSession();
135          Transaction tx = s.beginTransaction();
136  
137          // indicates user allowance for the data of the requested server
138          boolean userHasAllowanceForServer = false;
139  
140          try {
141  
142              // retrieve the requested statistics server
143              StatisticsServer server = loadStatisticsServer(
144                      serverIdFromCommand(c), s);
145  
146              // check user allowance
147              userHasAllowanceForServer = hasRoleForServer(request.getUser(),
148                      server);
149  
150              // build result data
151              if (userHasAllowanceForServer) {
152                  resultData = buildViewResultData(durationFromCommand(c),
153                          resolutionFromCommand(c), server, s);
154              }
155  
156              tx.commit();
157          } catch (Exception e) {
158              tx.rollback();
159              throw new PulseException("Error: " + e.getLocalizedMessage(), e);
160          } finally {
161              s.close();
162          }
163  
164          // return error if user is not allowed for server
165          if (!userHasAllowanceForServer) {
166              return userIsNotAllowedEvent(request);
167          }
168  
169          // store current data for report-/chart-image-requests
170          putResultDataInSession(resultData, request.getSession());
171  
172          // output
173          request.getEventManager().addEvent(
174                  new XSLTOutputEvent(getConfiguration().getMainXSLHandle()));
175  
176          return newResult(getConfiguration(), resultData);
177      }
178  
179      /**
180       * Adds a {@code ForbiddenEvent} to the {@code EventManager} of the current
181       * {@code ServiceRequest} with the following message "You do not have the
182       * required roles to view the data for the requested server.".
183       * 
184       * @param request
185       *            the current {@code ServiceRequest}
186       * 
187       * @return {@code null}
188       */
189      protected final V userIsNotAllowedEvent(final ServiceRequest request) {
190          request.getEventManager().addEvent(new ForbiddenEvent());
191          return null;
192      }
193  
194      /**
195       * Loads the {@code AbstractAggregation}s of the given type <tt>clazz</tt>
196       * for the given {@code StatisticsServer} and the given {@code Duration}.
197       * 
198       * @param clazz
199       *            the {@code Class&lt;? extends AbstractAggregation&gt;} to be
200       *            loaded
201       * @param statisticsServer
202       *            the {@code StatisticsServer}
203       * @param duration
204       *            the {@code Duration}
205       * @param s
206       *            the Hibernate<sup>TM</sup>-{@code Session} to be used for
207       *            loading
208       * 
209       * @return a {@code List&lt;? extends AbstractAggregation&gt;}
210       */
211      @SuppressWarnings("unchecked")
212      protected final List<? extends AbstractAggregation> loadAggregations(
213              final Class<? extends AbstractAggregation> clazz,
214              final StatisticsServer statisticsServer, final Duration duration,
215              final Session s) {
216          return (List<? extends AbstractAggregation>) s
217                  .createQuery(
218                          "from " + clazz.getSimpleName() + " aggr where "
219                                  + "aggr.statisticsServer=? and "
220                                  + "aggr.startTime >= ? and aggr.endTime <= ? "
221                                  + "order by aggr.startTime")
222                  // server
223                  .setParameter(0, statisticsServer)
224                  // start
225                  .setParameter(1, duration.getStartMillis())
226                  // end
227                  .setParameter(2, duration.getEndMillis()).list();
228      }
229  
230      /**
231       * Loads and returns a {@code StatisticsServer} by given request-
232       * {@code Parameter} "<tt>serverId</tt>".
233       * 
234       * @param serverId
235       *            the request-{@code Parameter}
236       * @param s
237       *            a Hibernate<sup>TM</sup>-{@code Session} to be used for
238       *            loading
239       * 
240       * @return {@code StatisticsServer} if one could be loaded, {@code null}
241       *         otherwise
242       */
243      protected final StatisticsServer loadStatisticsServer(final long serverId,
244              final Session s) {
245          if (serverId == 0L) {
246              return null;
247          }
248          return (StatisticsServer) s.get(StatisticsServer.class, serverId);
249      }
250  
251      /**
252       * Builds a {@code Duration} from the given {@code Command} if the given
253       * <tt>command.getParameter("duration") != {@code null}</tt>.
254       * <p>
255       * If no duration can be build from request, the default {@code Duration} as
256       * in set in {@code &lt;T&gt;} based on the current time-stamp will be
257       * returned.
258       * </p>
259       * 
260       * @param command
261       *            the {@code Command} to build the {@code Duration} from
262       * 
263       * @return the {@code Duration}
264       * 
265       * @throws PulseException
266       *             if a request-parameter for the "duration" is given, but
267       *             cannot be converted to a valid {@code Duration}
268       */
269      protected final Duration durationFromCommand(final Command command) {
270  
271          Parameter duration = command.getParameter("duration");
272  
273          if (duration == null || duration.getFirstValue() == null) {
274              return Period.getDuration(getConfiguration()
275                      .getDefaultStatisticalViewPeriod(), System
276                      .currentTimeMillis());
277          }
278  
279          String durationString = duration.getFirstValue();
280          if (Period.valueof(durationString) != null) {
281              return Period.getDuration(Period.valueof(durationString),
282                      System.currentTimeMillis());
283          }
284  
285          String[] timestamps = durationString.split("-");
286  
287          // check
288          if (timestamps.length != 2) {
289              throw new PulseException("Invalid request-parameter duration=\""
290                      + duration.getFirstValue() + "\" given.");
291          }
292  
293          // try to build duration from parameter
294  
295          long one, two;
296          try {
297              one = Long.parseLong(timestamps[0]);
298              two = Long.parseLong(timestamps[1]);
299          } catch (NumberFormatException e) {
300              throw new PulseException("Invalid request-parameter duration=\""
301                      + duration.getFirstValue() + "\" given: "
302                      + e.getLocalizedMessage(), e);
303          }
304  
305          if (one < two) {
306              return new Duration(Period.Level.level(one, Period.DAY,
307                      Period.Level.START), Period.Level.level(two, Period.DAY,
308                      Period.Level.END));
309          }
310  
311          return new Duration(Period.Level.level(two, Period.DAY,
312                  Period.Level.START), Period.Level.level(one, Period.DAY,
313                  Period.Level.END));
314  
315      }
316  
317      /**
318       * Builds a {@code Duration} from the given {@code Command} if the given
319       * <tt>command.getParameter("resolution") != {@code null}</tt>.
320       * <p>
321       * If no {@code Period} can be build from request, the default
322       * {@code Period} as in set in {@code &lt;T&gt;} will be returned.
323       * </p>
324       * <p>
325       * <strong>NOTE:</strong> {@code &lt;T&gt;} does not necessarily have to
326       * have a default "data resolution" {@code Period} to be set.
327       * </p>
328       * 
329       * @param command
330       *            the {@code Command} to build the {@code Period} from
331       * 
332       * @return the {@code Period}, or {@code null} if none could be retrieved or
333       *         build from given command
334       * 
335       * @throws PulseException
336       *             if a request-parameter for the "resolution" is given, but
337       *             cannot be converted to a valid {@code Period}
338       */
339      protected final Period resolutionFromCommand(final Command command) {
340  
341          if (command == null) {
342              // return default configured period
343              return getConfiguration()
344                      .getDefaultStatisticalDataResolutionPeriod();
345          }
346  
347          // retrieve request parameter "resolution"
348          Parameter resolution = command.getParameter("resolution");
349  
350          if (resolution == null || resolution.getFirstValue().equals("")) {
351              // return default configured period
352              return getConfiguration()
353                      .getDefaultStatisticalDataResolutionPeriod();
354          }
355  
356          try {
357  
358              // build period from command
359              Period resolutionPeriod = Period.valueof(command
360                      .getParameter("resolution").getFirstValue().trim()
361                      .toUpperCase());
362  
363              if (resolutionPeriod.equals(Period.UNDEFINED)) {
364                  // return default configured period
365                  return getConfiguration()
366                          .getDefaultStatisticalDataResolutionPeriod();
367              }
368  
369              // return period from command
370              return resolutionPeriod;
371  
372          } catch (Exception e) {
373              throw new PulseException("Failed building a Period from \""
374                      + command.getParameter("resolution").getFirstValue()
375                      + "\": " + e.getLocalizedMessage(), e);
376          }
377  
378      }
379  
380      /**
381       * Retrieves the "height" as integer from the given {@code Command}.
382       * 
383       * @param command
384       *            the {@code Command}
385       * 
386       * @return the positive integer "height", if a "height" could be retrieved
387       *         and built from the given command, the default
388       *         {@code AbstractStatisticsViewController.DEFAULT_CHART_HEIGHT}
389       *         otherwise and on internal {@code NumberFormatException}
390       */
391      protected final int chartHeightFromCommand(final Command command) {
392          Integer i = integerFromCommand(command, "height");
393          if (i == null || i < 0) {
394              return AbstractStatisticsViewController.DEFAULT_CHART_HEIGHT;
395          }
396          return i;
397      }
398  
399      /**
400       * Retrieves the "width" as integer from the given {@code Command}.
401       * 
402       * @param command
403       *            the {@code Command}
404       * 
405       * @return the positive integer "width", if a "width" could be retrieved and
406       *         built from the given command, the default
407       *         {@code AbstractStatisticsViewController.DEFAULT_CHART_WIDTH}
408       *         otherwise and on internal {@code NumberFormatException}
409       */
410      protected final int chartWidthFromCommand(final Command command) {
411          Integer i = integerFromCommand(command, "width");
412          if (i == null || i < 0) {
413              return AbstractStatisticsViewController.DEFAULT_CHART_WIDTH;
414          }
415          return i;
416      }
417  
418      /**
419       * Retrieves the "serverId" as long from the given {@code Command}.
420       * 
421       * @param command
422       *            the {@code Command}
423       * 
424       * @return the positive long "serverId", if a "serverId" could be retrieved
425       *         and built from the given command, the default
426       *         {@code AbstractStatisticsViewController.NO_SERVERID} otherwise
427       *         and on internal {@code NumberFormatException}
428       */
429      protected final long serverIdFromCommand(final Command command) {
430          Integer i = integerFromCommand(command, "serverId");
431          if (i == null || i < 0) {
432              return AbstractStatisticsViewController.NO_SERVERID;
433          }
434          return i;
435      }
436  
437      /**
438       * Retrieves an integer by <tt>parameterName</tt> from the given
439       * {@code Command}.
440       * 
441       * @param command
442       *            the {@code Command}
443       * @param parameterName
444       *            the name of the {@code Parameter} to look for
445       * 
446       * @return the "height" as a positive integer if a "height" could be
447       *         retrieved and built from the given command, the default
448       *         {@code AbstractStatisticsViewController.DEFAULT_CHART_HEIGHT}
449       *         otherwise and on internal {@code NumberFormatException}
450       */
451      protected final Integer integerFromCommand(final Command command,
452              final String parameterName) {
453          try {
454              return Integer.parseInt(command.getParameter(parameterName)
455                      .getFirstValue());
456          } catch (NumberFormatException e) {
457              return null;
458          }
459      }
460  
461      /**
462       * Adds the given {@code AbstractStatisticsViewControllerResultData} to the
463       * current request-session and stores it under the following key:
464       * <p>
465       * <tt>"fqn.of.the.current.{@code AbstractStatisticsViewController}#resultData.{@code StatisticsServer}.id"</tt>
466       * </p>
467       * .
468       * 
469       * @param resultData
470       *            the {@code AbstractStatisticsViewControllerResultData}
471       * @param session
472       *            the current {@code ServiceSession}
473       */
474      protected final void putResultDataInSession(final U resultData,
475              final ServiceSession session) {
476          if (session == null) {
477              return;
478          }
479          if (resultData != null) {
480              session.setAttribute(getClass().getCanonicalName() + "#"
481                      + resultData.getStatisticsServer().getId(), resultData);
482          }
483      }
484  
485      /**
486       * Retrieves the {@code AbstractStatisticsViewControllerResultData} from the
487       * current {@code ServiceSession} for the given id of a
488       * {@code StatisticsServer}.
489       * 
490       * @param session
491       *            the current {@code ServiceSession}
492       * @param statisticsServerId
493       *            the id of the {@code StatisticsServer}
494       * 
495       * @return the {@code AbstractStatisticsViewControllerResultData}, or
496       *         {@code null} if no
497       *         {@code AbstractStatisticsViewControllerResultData}
498       */
499      @SuppressWarnings("unchecked")
500      protected final U fetchResultDataFromSession(final ServiceSession session,
501              final long statisticsServerId) {
502          if (session == null) {
503              return null;
504          }
505          return (U) session.getAttribute(getClass().getCanonicalName() + "#"
506                  + statisticsServerId);
507      }
508  
509      /**
510       * Retrieves the {@code AbstractStatisticsViewControllerResultData} from the
511       * current {@code ServiceSession} for the given id of a
512       * {@code StatisticsServer}.
513       * 
514       * @param session
515       *            the current {@code ServiceSession}
516       * @param statisticsServerId
517       *            the id of the {@code StatisticsServer}
518       */
519      protected final void removeResultDataFromSession(
520              final ServiceSession session, final long statisticsServerId) {
521          if (session == null) {
522              return;
523          }
524          LOGGER.info("removing session data: {}#{}", getClass()
525                  .getCanonicalName(), statisticsServerId);
526          session.removeAttribute(getClass().getCanonicalName() + "#"
527                  + statisticsServerId);
528      }
529  
530      /**
531       * Returns the corresponding {@code RegularTimePeriod} for the given
532       * {@code Period}.
533       * 
534       * @param dataResolution
535       *            the {@code Period}
536       * 
537       * @return a {@code RegularTimePeriod}, {@code null} if no
538       *         {@code RegularTimePeriod} could be mapped to the given
539       *         {@code Period}
540       */
541      protected final Class<? extends RegularTimePeriod> determineJFreeChartTimePeriod(
542              final Period dataResolution) {
543          switch (dataResolution) {
544          case HOUR:
545              return Hour.class;
546          case DAY:
547              return Day.class;
548          case WEEK:
549              return Week.class;
550          case MONTH:
551              return Month.class;
552          case YEAR:
553              return Year.class;
554          default:
555              return null;
556          }
557      }
558  
559      /**
560       * Performs the creation of the PDF-file and adds the required
561       * {@code PDFOutputEvent} to the {@code EventManager}.
562       * 
563       * @param result
564       *            the actual implementation of:
565       *            {@code AbstractStatisticsViewControllerResult}
566       * @param fileMap
567       *            a map of files for the creation of the report PDF
568       * @param request
569       *            the current {@code ServiceRequest}
570       * 
571       * @return the (modified) {@code AbstractStatisticsViewControllerResult} as
572       *         used for the creation of the PDF
573       */
574      protected final V outputReportPDF(final V result,
575              final Map<String, File> fileMap, final ServiceRequest request) {
576  
577          // build PDF-output-event
578          PDFOutputEvent output = new PDFOutputEvent(getConfiguration()
579                  .getReportPDFXSLHandle(), fileNameForReport(result
580                  .getResultData().getStatisticsServer(), "pdf"),
581                  Disposition.ATTACHED);
582  
583          // add the files
584          if (fileMap != null) {
585              for (Map.Entry<String, File> entry : fileMap.entrySet()) {
586                  if (!entry.getValue().isDirectory()) {
587                      output.addTemoraryResource(entry.getValue());
588                  }
589                  result.put(entry.getKey(), entry.getValue().toURI().toString());
590              }
591          }
592  
593          // add event
594          request.getEventManager().addEvent(output);
595  
596          // output
597          return result;
598  
599      }
600  
601      /**
602       * Creates a string to name the actual report with.
603       * 
604       * @param server
605       *            the {@code StatisticsServer}
606       * @param suffix
607       *            the file name suffix
608       * 
609       * @return a string to be used for naming the report file
610       */
611      protected final String fileNameForReport(final StatisticsServer server,
612              final String suffix) {
613          SimpleDateFormat format = new SimpleDateFormat("EEE-MMM-d-yyyy",
614                  Locale.getDefault());
615          if (suffix == null || suffix.equals("")) {
616              return new StringBuilder(getConfiguration().getReportPDFPrefix())
617                      .append('.').append(server.getHostName()).append('.')
618                      .append(format.format(new Date())).toString();
619          }
620          return new StringBuilder(getConfiguration().getReportPDFPrefix())
621                  .append('.').append(server.getHostName()).append('.')
622                  .append(format.format(new Date())).append('.').append(suffix)
623                  .toString();
624      }
625  
626  }
627