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 importorg.torweg.pulse.component.statistics.model.StatisticsServer;
36 importorg.torweg.pulse.component.statistics.model.aggregation.AbstractAggregation;
37 importorg.torweg.pulse.invocation.lifecycle.Lifecycle;
38 importorg.torweg.pulse.service.PulseException;
39 importorg.torweg.pulse.service.event.ForbiddenEvent;
40 importorg.torweg.pulse.service.event.PDFOutputEvent;
41 importorg.torweg.pulse.service.event.XSLTOutputEvent;
42 importorg.torweg.pulse.service.event.Event.Disposition;
43 importorg.torweg.pulse.service.request.Command;
44 importorg.torweg.pulse.service.request.Parameter;
45 importorg.torweg.pulse.service.request.ServiceRequest;
46 importorg.torweg.pulse.service.request.ServiceSession;
47 importorg.torweg.pulse.util.time.Duration;
48 importorg.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 publicabstractclass AbstractStatisticsViewController<T extendsAbstractStatisticsViewControllerConfiguration, U extendsAbstractStatisticsViewControllerResultData, V extendsAbstractStatisticsViewControllerResult<U>>
68 extendsAbstractStatisticsController<T, V> {
69 70 /**
71 * The default chart width - <tt>600</tt> - being used if none is specified.
72 */73 protectedstaticfinalint DEFAULT_CHART_WIDTH = 600;
74 75 /**
76 * The default chart height - <tt>400</tt> - being used if none is
77 * specified.
78 */79 protectedstaticfinalint DEFAULT_CHART_HEIGHT = 400;
80 81 /**
82 * The default server id - <tt>0L</tt> - being used if none is specified.
83 */84 protectedstaticfinallong 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 <U>}
101 */102 protectedabstract U buildViewResultData(finalDuration duration,
103 final Period dataResolution, finalStatisticsServer server,
104 final Session s);
105 106 /**
107 * Factory method to retrieve an actual new result.
108 *
109 * @param configuration
110 * {@code <T>}
111 * @param data
112 * {@code <U>}
113 *
114 * @return {@code <V>}
115 */116 protectedabstract V newResult(T configuration, U data);
117 118 /**
119 * Builds the {@code <V<U>>} 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 <V>}
128 */129 protectedfinal V initView(finalServiceRequest request,
130 finalboolean 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 thrownewPulseException("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 newXSLTOutputEvent(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 protectedfinal V userIsNotAllowedEvent(finalServiceRequest request) {
190 request.getEventManager().addEvent(newForbiddenEvent());
191 returnnull;
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<? extends AbstractAggregation>} 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<? extends AbstractAggregation>}
210 */211 @SuppressWarnings("unchecked")
212 protectedfinal List<? extendsAbstractAggregation> loadAggregations(
213 final Class<? extendsAbstractAggregation> clazz,
214 finalStatisticsServer statisticsServer, finalDuration duration,
215 final Session s) {
216 return (List<? extendsAbstractAggregation>) 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 protectedfinalStatisticsServer loadStatisticsServer(finallong serverId,
244 final Session s) {
245 if (serverId == 0L) {
246 returnnull;
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 <T>} 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 protectedfinalDuration durationFromCommand(finalCommand 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 thrownewPulseException("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 thrownewPulseException("Invalid request-parameter duration=\""301 + duration.getFirstValue() + "\" given: "302 + e.getLocalizedMessage(), e);
303 }
304 305 if (one < two) {
306 returnnewDuration(Period.Level.level(one, Period.DAY,
307 Period.Level.START), Period.Level.level(two, Period.DAY,
308 Period.Level.END));
309 }
310 311 returnnewDuration(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 <T>} will be returned.
323 * </p>
324 * <p>
325 * <strong>NOTE:</strong> {@code <T>} 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 protectedfinal Period resolutionFromCommand(finalCommand 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 thrownewPulseException("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 protectedfinalint chartHeightFromCommand(finalCommand 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 protectedfinalint chartWidthFromCommand(finalCommand 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 protectedfinallong serverIdFromCommand(finalCommand 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 protectedfinal Integer integerFromCommand(finalCommand command,
452 final String parameterName) {
453 try {
454 return Integer.parseInt(command.getParameter(parameterName)
455 .getFirstValue());
456 } catch (NumberFormatException e) {
457 returnnull;
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 protectedfinalvoid putResultDataInSession(final U resultData,
475 finalServiceSession 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 protectedfinal U fetchResultDataFromSession(finalServiceSession session,
501 finallong statisticsServerId) {
502 if (session == null) {
503 returnnull;
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 protectedfinalvoid removeResultDataFromSession(
520 finalServiceSession session, finallong 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 protectedfinal 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 returnnull;
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 protectedfinal V outputReportPDF(final V result,
575 final Map<String, File> fileMap, finalServiceRequest request) {
576 577 // build PDF-output-event
578 PDFOutputEvent output = newPDFOutputEvent(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 protectedfinal String fileNameForReport(finalStatisticsServer server,
612 final String suffix) {
613 SimpleDateFormat format = new SimpleDateFormat("EEE-MMM-d-yyyy",
614 Locale.getDefault());
615 if (suffix == null || suffix.equals("")) {
616 returnnew StringBuilder(getConfiguration().getReportPDFPrefix())
617 .append('.').append(server.getHostName()).append('.')
618 .append(format.format(new Date())).toString();
619 }
620 returnnew StringBuilder(getConfiguration().getReportPDFPrefix())
621 .append('.').append(server.getHostName()).append('.')
622 .append(format.format(new Date())).append('.').append(suffix)
623 .toString();
624 }
625 626 }
627