/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos 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; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.controls.reports.statistics;
import java.lang.reflect.Constructor;
import java.util.Calendar;
import java.util.Collection;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import nl.strohalm.cyclos.annotations.Inject;
import nl.strohalm.cyclos.controls.ActionContext;
import nl.strohalm.cyclos.controls.BaseQueryAction;
import nl.strohalm.cyclos.controls.reports.statistics.graphs.StatisticalDataProducer;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.SystemAccountType;
import nl.strohalm.cyclos.entities.accounts.transactions.PaymentFilter;
import nl.strohalm.cyclos.entities.accounts.transactions.PaymentFilterQuery;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.GroupFilter;
import nl.strohalm.cyclos.entities.groups.GroupFilterQuery;
import nl.strohalm.cyclos.entities.groups.GroupQuery;
import nl.strohalm.cyclos.entities.reports.StatisticalQuery;
import nl.strohalm.cyclos.entities.reports.StatisticsWhatToShow;
import nl.strohalm.cyclos.entities.reports.ThroughTimeRange;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.entities.settings.events.LocalSettingsChangeListener;
import nl.strohalm.cyclos.entities.settings.events.LocalSettingsEvent;
import nl.strohalm.cyclos.services.accounts.AccountTypeService;
import nl.strohalm.cyclos.services.accounts.SystemAccountTypeQuery;
import nl.strohalm.cyclos.services.groups.GroupFilterService;
import nl.strohalm.cyclos.services.stats.StatisticalResultDTO;
import nl.strohalm.cyclos.services.stats.StatisticalService;
import nl.strohalm.cyclos.services.transfertypes.PaymentFilterService;
import nl.strohalm.cyclos.utils.DateHelper;
import nl.strohalm.cyclos.utils.Month;
import nl.strohalm.cyclos.utils.NamedPeriod;
import nl.strohalm.cyclos.utils.Period;
import nl.strohalm.cyclos.utils.Quarter;
import nl.strohalm.cyclos.utils.RequestHelper;
import nl.strohalm.cyclos.utils.binding.BeanBinder;
import nl.strohalm.cyclos.utils.binding.DataBinder;
import nl.strohalm.cyclos.utils.binding.DataBinderHelper;
import nl.strohalm.cyclos.utils.binding.PropertyBinder;
import nl.strohalm.cyclos.utils.binding.SimpleCollectionBinder;
import nl.strohalm.cyclos.utils.conversion.ReferenceConverter;
import nl.strohalm.cyclos.utils.query.QueryParameters;
import org.apache.commons.collections.CollectionUtils;
/**
* The common ancestor for all statistics actions. Defines some general methods for statistics.
*
* @author Rinke
*
*/
public abstract class StatisticsAction extends BaseQueryAction implements LocalSettingsChangeListener {
/**
* Enumeration to indicate what type of statistics will be shown
*/
public enum StatisticsType {
KEY_DEVELOPMENTS, MEMBER_ACTIVITIES, FINANCES, TAXES;
}
/**
* binds the common fields to the form, such as periods and filters. Method to be called from the initDataBinder method
*
* @param binder
* @param settings
*/
protected static void bindCommonFields(final BeanBinder<? extends StatisticalQuery> binder, final LocalSettings settings) {
binder.registerBinder("periodMain", DataBinderHelper.namedPeriodBinder(settings, "periodMain"));
binder.registerBinder("periodComparedTo", DataBinderHelper.namedPeriodBinder(settings, "periodComparedTo"));
binder.registerBinder("throughTimeRange", PropertyBinder.instance(ThroughTimeRange.class, "throughTimeRange"));
binder.registerBinder("initialMonth", PropertyBinder.instance(Month.class, "initialMonth"));
binder.registerBinder("finalMonth", PropertyBinder.instance(Month.class, "finalMonth"));
binder.registerBinder("initialQuarter", PropertyBinder.instance(Quarter.class, "initialQuarter"));
binder.registerBinder("finalQuarter", PropertyBinder.instance(Quarter.class, "finalQuarter"));
binder.registerBinder("initialYear", PropertyBinder.instance(Integer.class, "initialYear"));
binder.registerBinder("initialMonthYear", PropertyBinder.instance(Integer.class, "initialMonthYear"));
binder.registerBinder("initialQuarterYear", PropertyBinder.instance(Integer.class, "initialQuarterYear"));
binder.registerBinder("finalYear", PropertyBinder.instance(Integer.class, "finalYear"));
binder.registerBinder("finalMonthYear", PropertyBinder.instance(Integer.class, "finalMonthYear"));
binder.registerBinder("finalQuarterYear", PropertyBinder.instance(Integer.class, "finalQuarterYear"));
binder.registerBinder("paymentFilter", PropertyBinder.instance(PaymentFilter.class, "paymentFilter", ReferenceConverter.instance(PaymentFilter.class)));
binder.registerBinder("paymentFilters", SimpleCollectionBinder.instance(PaymentFilter.class, "paymentFilters", ReferenceConverter.instance(PaymentFilter.class)));
binder.registerBinder("groupFilters", SimpleCollectionBinder.instance(GroupFilter.class, "groupFilters"));
binder.registerBinder("groups", SimpleCollectionBinder.instance(Group.class, "groups"));
binder.registerBinder("systemAccountFilter", PropertyBinder.instance(SystemAccountType.class, "systemAccountFilter"));
binder.registerBinder("whatToShow", PropertyBinder.instance(StatisticsWhatToShow.class, "whatToShow"));
}
/**
* the Service. Child classes should assign this via
*
* @inject setBlaService, calling StatisticsAction.setStatisticalService.
*/
private StatisticalService statisticalService;
private PaymentFilterService paymentFilterService;
private AccountTypeService accountTypeService;
private GroupFilterService groupFilterService;
private DataBinder<? extends StatisticalQuery> dataBinder;
public DataBinder<? extends StatisticalQuery> getDataBinder() {
if (dataBinder == null) {
final LocalSettings settings = settingsService.getLocalSettings();
dataBinder = initDataBinder(settings);
}
return dataBinder;
}
/**
* Returns the statistics type related to the action
*/
public abstract StatisticsType getStatisticsType();
/**
* each subclass is forced to bind datafields via this method
*
* @param settings
*/
public abstract DataBinder<? extends StatisticalQuery> initDataBinder(final LocalSettings settings);
/*
* takes care that the transfer type filter box filled with appropriate transfer types. Method to be called from the prepareForm method. Currently
* not used, but may be in future. If never used in future, then remove this and all associated.
*
* @param context UNCOMMENT IF USED IN FUTURE
*/
/*
* protected void applyTransferTypeFilter(final ActionContext context) { final TransferTypeQuery ttQuery = resolveTransferTypeQuery(context); if
* (ttQuery != null) { final List<TransferType> transferTypes = transferTypeService.search(ttQuery); final HttpServletRequest request =
* context.getRequest(); request.setAttribute("transferTypeList", transferTypes); } }
*/
@Override
public void onLocalSettingsUpdate(final LocalSettingsEvent event) {
super.onLocalSettingsUpdate(event);
dataBinder = null;
}
@Inject
public void setAccountTypeService(final AccountTypeService accountTypeService) {
this.accountTypeService = accountTypeService;
}
@Inject
public void setGroupFilterService(final GroupFilterService groupFilterService) {
this.groupFilterService = groupFilterService;
}
@Inject
public void setPaymentFilterService(final PaymentFilterService paymentFilterService) {
this.paymentFilterService = paymentFilterService;
}
/**
* takes care that a group filter box is filled with appropriate values. Method to be called from the prepareForm method.
*
* @param request
*/
protected void applyGroupFilter(final HttpServletRequest request) {
final GroupQuery groupQuery = new GroupQuery();
groupQuery.setNatures(Group.Nature.MEMBER, Group.Nature.BROKER);
request.setAttribute("groups", groupService.search(groupQuery));
final GroupFilterQuery groupFilterQuery = new GroupFilterQuery();
// no need to set anything on the GroupFilterQuery, as stats fall outside the scope of group managing permissions
final Collection<GroupFilter> groupFilters = groupFilterService.search(groupFilterQuery);
if (CollectionUtils.isNotEmpty(groupFilters)) {
request.setAttribute("groupFilters", groupFilters);
}
}
/**
* takes care that the payment filter box filled with appropriate payment filters. Method to be called from the prepareForm method
*
* @param request
*/
protected void applyPaymentFilter(final HttpServletRequest request) {
final PaymentFilterQuery pfQuery = new PaymentFilterQuery();
pfQuery.setContext(PaymentFilterQuery.Context.REPORT);
final List<PaymentFilter> paymentFilters = paymentFilterService.search(pfQuery);
request.setAttribute("paymentFilterList", paymentFilters);
}
/**
* takes care that the system account filter box filled with appropriate system accounts. Method to be called from the prepareForm method
*
* @param request
*/
protected void applySystemAccountFilter(final HttpServletRequest request) {
final List<? extends AccountType> systemAccounts = accountTypeService.search(new SystemAccountTypeQuery());
request.setAttribute("systemAccounts", systemAccounts);
}
/**
* Takes care that both the named periods are filled with default values. To be called from the prepareForm method.
*
* @param query
* @param form
*/
protected void bindPeriods(final StatisticalQuery query, final StatisticsForm form) {
if (query.getPeriodMain().getEnd() == null) {
final NamedPeriod periodMain = NamedPeriod.getLastQuarterPeriod();
final NamedPeriod periodComparedTo = periodMain.getOneYearEarlier();
bindPeriod("periodMain", form, periodMain);
bindPeriod("periodComparedTo", form, periodComparedTo);
}
}
// call this method via super() from the inhereting classes.
@Override
protected void executeQuery(final ActionContext context, final QueryParameters queryParameters) {
final StatisticalQuery query = (StatisticalQuery) queryParameters;
final HttpServletRequest request = context.getRequest();
request.setAttribute("statisticsType", getStatisticsType());
// If is through the time, calculate periods and set into the queryparams
if (query.getWhatToShow() == StatisticsWhatToShow.THROUGH_TIME) {
final ThroughTimeRange throughTimeRange = query.getThroughTimeRange();
final Period period = getPeriodByTimeRange(query);
final Period[] periods = DateHelper.getPeriodsThroughTheTime(period, throughTimeRange);
query.setPeriods(periods);
}
// if groups are empty, but groupFilters are used, put all the groups from the groupFilter in the groups.
final Collection<GroupFilter> groupFilters = query.getGroupFilters();
final boolean hasGroupFilters = CollectionUtils.isNotEmpty(groupFilters);
final boolean hasGroups = CollectionUtils.isNotEmpty(query.getGroups());
if (hasGroupFilters && !hasGroups) {
final Set<Group> groupsFromFilters = new HashSet<Group>();
if (hasGroupFilters) {
// Get all groups from selected group filters
for (GroupFilter groupFilter : groupFilters) {
groupFilter = groupFilterService.load(groupFilter.getId(), GroupFilter.Relationships.GROUPS);
groupsFromFilters.addAll(groupFilter.getGroups());
}
query.setGroups(groupsFromFilters);
}
}
}
/**
* gets the StatisticalService in its basic form as an instance of the StatisticalService class. Child classes should create a
* getStatisticalService() method casting the StatisticalService instance to one of its subclasses
*
* @return StatisticalService
*/
protected StatisticalService getBaseStatisticalService() {
return statisticalService;
}
/*
* Create a period with its 'begin' and 'end' date corresponding to the first and second param on the period selection for months, quarters and
* years.
*/
protected Period getPeriodByTimeRange(final StatisticalQuery queryParameters) {
final ThroughTimeRange throughTimeRange = queryParameters.getThroughTimeRange();
Calendar calendarBegin = null;
Calendar calendarEnd = null;
if (throughTimeRange == ThroughTimeRange.MONTH) {
calendarBegin = new GregorianCalendar(queryParameters.getInitialMonthYear(), queryParameters.getInitialMonth().getValue(), 1);
final Calendar calendarEndAux = new GregorianCalendar(queryParameters.getFinalMonthYear(), queryParameters.getFinalMonth().getValue(), 1);
calendarEnd = new GregorianCalendar(queryParameters.getFinalMonthYear(), queryParameters.getFinalMonth().getValue(), calendarEndAux.getActualMaximum(Calendar.DAY_OF_MONTH), 23, 59, 59);
} else if (throughTimeRange == ThroughTimeRange.QUARTER) {
final Quarter initialQuarter = queryParameters.getInitialQuarter();
final Quarter finalQuarter = queryParameters.getFinalQuarter();
calendarBegin = new GregorianCalendar(queryParameters.getInitialQuarterYear(), initialQuarter.getValue() * 3 - 3, 1);
final Calendar calendarEndAux = new GregorianCalendar(queryParameters.getFinalQuarterYear(), finalQuarter.getValue() * 3 - 3, 1);
calendarEnd = new GregorianCalendar(queryParameters.getFinalQuarterYear(), finalQuarter.getValue() * 3 - 3, calendarEndAux.getActualMaximum(Calendar.DAY_OF_MONTH), 23, 59, 59);
} else if (throughTimeRange == ThroughTimeRange.YEAR) {
calendarBegin = new GregorianCalendar(queryParameters.getInitialYear(), 0, 1);
calendarEnd = new GregorianCalendar(queryParameters.getFinalYear(), 11, 31, 23, 59, 59);
}
final Period period = new Period(calendarBegin, calendarEnd);
return period;
}
/**
* This prepares the form. Some common fields are taken care of (Periods); all filters must be assigned by the descendant class prepareForm
* method, which should call return super.prepareForm(context);
*/
@Override
protected QueryParameters prepareForm(final ActionContext context) {
final StatisticsForm form = context.getForm();
final StatisticalQuery query = getDataBinder().readFromString(form.getQuery());
bindPeriods(query, form);
// Send enums to JSP
final HttpServletRequest request = context.getRequest();
RequestHelper.storeEnum(request, StatisticsWhatToShow.class, "whatToShow");
RequestHelper.storeEnum(request, ThroughTimeRange.class, "throughTimeRange");
RequestHelper.storeEnum(request, Month.class, "months");
RequestHelper.storeEnum(request, Quarter.class, "quarters");
// Set default through time range
if (form.getQuery("throughTimeRange") == null) {
form.setQuery("throughTimeRange", ThroughTimeRange.MONTH);
}
// Set default initial and final months and years
if (form.getQuery("initialMonth") == null) {
final Map<String, Object> completedMonthAndYear = DateHelper.getLastCompletedMonthAndYear();
final int lastCompletedMonth = (Integer) completedMonthAndYear.get("month");
final int lastCompletedMonthYear = (Integer) completedMonthAndYear.get("year");
form.setQuery("initialMonth", lastCompletedMonth);
form.setQuery("initialMonthYear", lastCompletedMonthYear - 1);
form.setQuery("finalMonth", lastCompletedMonth);
form.setQuery("finalMonthYear", lastCompletedMonthYear);
}
// Set default initial and final quarters and years
if (form.getQuery("initialQuarter") == null) {
final Map<String, Object> completedQuarterAndYear = DateHelper.getLastCompletedQuarterAndYear();
final Quarter lastCompletedQuarter = (Quarter) completedQuarterAndYear.get("quarter");
final int lastCompletedQuarterYear = (Integer) completedQuarterAndYear.get("year");
form.setQuery("initialQuarter", lastCompletedQuarter);
form.setQuery("initialQuarterYear", lastCompletedQuarterYear - 1);
form.setQuery("finalQuarter", lastCompletedQuarter);
form.setQuery("finalQuarterYear", lastCompletedQuarterYear);
}
// Set default initial and final years
if (form.getQuery("initialYear") == null) {
final Calendar date = elementService.getFirstMemberActivationDate();
int firstYear = 0;
if (date != null) {
firstYear = date.get(Calendar.YEAR);
} else {
firstYear = Calendar.getInstance().get(Calendar.YEAR);
}
final int currentYear = Calendar.getInstance().get(Calendar.YEAR);
int lastYear = currentYear - 1;
if (firstYear > lastYear) {
// The systems started to run this year
firstYear = currentYear;
lastYear = currentYear;
}
form.setQuery("initialYear", firstYear);
form.setQuery("finalYear", lastYear);
}
return query;
}
/**
* This method creates the StatisticalDataProducer which will be read by the jsp. It is a factory, so dependant of the producerClass parameter, it
* returns any subtype of StatisticalDataProducer (or StatisticalDataProducer itself).
*
* @param rawDataObject the raw Data object from cyclos3
* @param context the action context
* @param producerClass this must be a StatisticalDataProducer or one of its subclasses. This parameter determines the type of the returned
* DataProducer.
* @return StatisticalDataProducer (or one of its subclasses), a wrapper type around the raw data.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
protected StatisticalDataProducer producerFactory(final StatisticalResultDTO rawDataObject, final ActionContext context, final Class producerClass) {
// create a StatisticalDataProducer of the correct class by getting the
// constructor via reflection
final Class[] argumentClasses = new Class[] { StatisticalResultDTO.class, ActionContext.class };
final Object[] constructorArguments = new Object[] { rawDataObject, context };
Constructor producerConstructor = null;
StatisticalDataProducer producer = null;
try {
producerConstructor = producerClass.getConstructor(argumentClasses);
producer = (StatisticalDataProducer) producerConstructor.newInstance(constructorArguments);
} catch (final Exception e) {
// in case of any silly error because of the use of reflection, just
// use the base type
e.printStackTrace();
producer = new StatisticalDataProducer(rawDataObject, context);
}
final LocalSettings settings = settingsService.getLocalSettings();
if (producer != null) {
producer.setSettings(settings);
}
return producer;
}
/**
* builds the query for getting the transfer types, to fill the transfer type drop down. Not used at present, maybe in future? If not, then
* remove.
*
* @param context
* @return always null; subclass to get a real query.
*/
protected TransferTypeQuery resolveTransferTypeQuery(final ActionContext context) {
return null;
}
/**
* basic setter for the statistical Services, to be called by child classes with the inject annotation.
*
* @param statisticalService
*/
protected void setStatisticalService(final StatisticalService statisticalService) {
this.statisticalService = statisticalService;
}
/**
* validates the following:
* <ul>
* <li>if any item checkbox in the form is selected.
* <li>if correct syntax for through time fields is entered (no end date smaller than start date; year fields not empty)
* <li>if the maximum number of requested data points is not exceeded. This max number is set via the service, and set in the prepareform method
* of the action
* <li>in case the paymentFilters multi drop down is used: if the maximum number of payment filters is not exceeded.
* <li>in case the paymentFilters multi drop down is used: if there is there is no overlap in the selected payment filters
* <li>there is at least one payment filter selected.
* </ul>
*
*/
@Override
protected void validateForm(final ActionContext context) {
final StatisticsForm form = context.getForm();
final StatisticalQuery query = getDataBinder().readFromString(form.getQuery());
statisticalService.validate(query);
}
/*
* Helper method fills a NamedPeriod instance with its default value Called from bindPeriods @param name @param form @param period
*/
private void bindPeriod(final String name, final StatisticsForm form, final NamedPeriod period) {
final LocalSettings settings = settingsService.getLocalSettings();
final BeanBinder<NamedPeriod> periodBinder = DataBinderHelper.namedPeriodBinder(settings, name);
periodBinder.writeAsString(form.getQuery(), period);
}
}