package fi.utu.ville.exercises.helpers; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import com.vaadin.ui.Component; import com.vaadin.ui.Label; import com.vaadin.ui.Table; import com.vaadin.ui.VerticalLayout; import fi.utu.ville.exercises.model.StatisticalSubmissionInfo; import fi.utu.ville.exercises.model.StatisticsInfoColumn; import fi.utu.ville.exercises.model.SubmissionInfo; import fi.utu.ville.exercises.model.SubmissionStatisticsGiver; import fi.utu.ville.standardutils.Localizer; import fi.utu.ville.standardutils.StandardUIConstants; /** * <p> * A helper class containing method for filtering {@link StatisticalSubmissionInfo}-objects and with implementors of {@link StatSubmInfoFilter} -interface. * </p> * <p> * Also contains standard implementations of {@link StatSubmInfoFilterConnector} for grouping {@link StatSubmInfoFilter}s ( {@link MatchAllFilter}, * {@link MatchAnyFilter} ). And {@link StatSubmInfoFilter}-implementor for inverting another filter ({@link InvertedFilter}), and for filtering by general * {@link StatisticalSubmissionInfo}-properties ( {@link DateFilter}, {@link EvaluationFilter}, {@link TimeOnTaskFilter}). * </p> * <p> * {@link SubmMatcher}-interface and {@link BySubmMatcher}-filter can be used to create {@link StatSubmInfoFilter}s that only care about properties of a * {@link SubmissionInfo}-object. * </p> * * @author Riku Haavisto * * @see StatSubmInfoFilterEditor * @see StatSubmInfoFilterTable * @see StatisticalSubmissionInfo * @see SubmissionStatisticsGiver * */ public class StatsGiverHelper { /** * <p> * Returns a table parsed from the {@link StatisticsInfoColumn}s. <b>It is absolutely necessary that all {@link StatisticsInfoColumn}s have same number of * data-objects (nulls are allowed) and match the order of input data.</b> * </p> * <p> * Table's columns will have as their property-id the index of certain {@link StatisticsInfoColumn} in the {@link StatisticsInfoColumn}-list. Table's rows * will have as their property-ids the index of the row's data in (all of the) {@link StatisticsInfoColumn #getDataObjects()}-lists. * </p> * <p> * The returned {@link Table} will contain the data, but otherwise is in default state. * </p> * * @param colsToLoad * {@link StatisticsInfoColumn} to load into a table * @return {@link Table} containing data from colsToLoad */ public static Table getTableFromStatCols( List<StatisticsInfoColumn<?>> colsToLoad) { Table res = new Table(); if (colsToLoad.isEmpty()) { return res; } // init table-props for (int i = 0; i < colsToLoad.size(); i++) { res.addContainerProperty(i, colsToLoad.get(i).getColDataType(), null); // using the index as column id avoids problems // with identical column-names... res.setColumnHeader(i, colsToLoad.get(i).getName()); } // all the StatisticsInfoColumns are required to hold // as many data-values (nulls are allowed, but dataObj-lists must // be of same size) int numOfRows = colsToLoad.get(0).getDataObjects().size(); // parse and load data for (int i = 0; i < numOfRows; i++) { Object[] cells = new Object[colsToLoad.size()]; for (int j = 0; j < colsToLoad.size(); j++) { cells[j] = colsToLoad.get(j).getDataObjects().get(i); } res.addItem(cells, i); } return res; } /** * Returns ids (indexes in the list) of columns for which {@link StatisticsInfoColumn #isExportable()} returns false. * * @param cols * list of {@link StatisticsInfoColumn}s * @return indices of columns that are not exportable */ public static Set<Integer> getNonExportableColIds( List<StatisticsInfoColumn<?>> cols) { Set<Integer> res = new HashSet<Integer>(); for (int i = 0; i < cols.size(); i++) { if (!cols.get(i).isExportable()) { res.add(i); } } return res; } /** * Parses a {@link Component} with simple legend about what kind of data each column has. * * @param forCols * list of {@link StatisticsInfoColumn}s * @param localizer * {@link Localizer} for localizing UI * @return a legend-{@link Component} parsed in default way */ public static Component getTableColsLegend( List<StatisticsInfoColumn<?>> forCols, Localizer localizer) { VerticalLayout res = new VerticalLayout(); res.addComponent(new Label(localizer .getUIText(StandardUIConstants.COLUMNS_LEGEND))); for (StatisticsInfoColumn<?> col : forCols) { res.addComponent(new Label("--")); res.addComponent(new Label(localizer.getUIText( StandardUIConstants.DESC_FOR_COL_X, col.getName()))); res.addComponent(new Label(col.getDescription())); } return res; } /** * Returns a new {@link List} containing all {@link StatisticalSubmissionInfo}-objects from the toFilter list matching {@link StatSubmInfoFilter} filter. * * @param toFilter * {@link List} of {@link StatisticalSubmissionInfo}-objects to be filtered * @param filter * {@link StatSubmInfoFilter} to use * @return {@link List} of {@link StatisticalSubmissionInfo}-objects from toFilter matching filter */ public static <S extends SubmissionInfo> List<StatisticalSubmissionInfo<S>> filterStatInfos( List<StatisticalSubmissionInfo<S>> toFilter, StatSubmInfoFilter<S> filter) { List<StatisticalSubmissionInfo<S>> res = new ArrayList<StatisticalSubmissionInfo<S>>(); for (StatisticalSubmissionInfo<S> anItem : toFilter) { if (filter.matches(anItem)) { res.add(anItem); } } return res; } /** * Implementor of {@link StatSubmInfoFilter}-interface can be used to filter {@link StatisticalSubmissionInfo}-objects. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public interface StatSubmInfoFilter<A extends SubmissionInfo> { /** * Test whether certain {@link StatisticalSubmissionInfo} matches this {@link StatSubmInfoFilter}. * * @param toTest * {@link StatisticalSubmissionInfo} to test * @return true if toTest matches this filter, false otherwise */ boolean matches(StatisticalSubmissionInfo<A> toTest); } /** * Implementors of {@link StatSubmInfoFilterConnector}-interface can connect several {@link StatSubmInfoFilter}-objects to a single * {@link StatSubmInfoFilter} . * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public interface StatSubmInfoFilterConnector<A extends SubmissionInfo> extends StatSubmInfoFilter<A> { /** * @return all the connected {@link StatSubmInfoFilter}s */ List<StatSubmInfoFilter<A>> getConnectedFilters(); /** * @param toConnect * adds {@link StatSubmInfoFilter} toConnect to this connector */ void connectFilter(StatSubmInfoFilter<A> toConnect); /** * @param toDisconnect * removes {@link StatSubmInfoFilter} toDisconnect from this connector */ void disconnectFilter(StatSubmInfoFilter<A> toDisconnect); } /** * {@link StatSubmInfoFilter} that filters {@link StatisticalSubmissionInfo} -objects by their {@link StatisticalSubmissionInfo #getDoneTime()} value. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class DateFilter<A extends SubmissionInfo> implements StatSubmInfoFilter<A> { private final long startMillis; private final long endMillis; /** * Constructs a new {@link DateFilter} matching given interval. You can use {@link Date #getTime()} to get milliseconds to use with this class. * * @param startMillis * start of accepted interval * @param endMillis * end of accepted interval */ public DateFilter(long startMillis, long endMillis) { this.startMillis = startMillis; this.endMillis = endMillis; if (this.startMillis >= this.endMillis) { throw new IllegalArgumentException( "StarMillis must be less than endMillis! Start was: " + this.startMillis + "; end was: " + this.endMillis); } } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { return (toTest.getDoneTime() >= startMillis) && (toTest.getDoneTime() <= endMillis); } } /** * {@link StatSubmInfoFilter} that filters {@link StatisticalSubmissionInfo} -objects by their {@link StatisticalSubmissionInfo #getEvalution()} value. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class EvaluationFilter<A extends SubmissionInfo> implements StatSubmInfoFilter<A> { private final double min; private final double max; /** * Constructs a new {@link EvaluationFilter} matching given evaluation interval. * * @param min * minimum matching evaluation value * @param max * maximum matching evaluation value */ public EvaluationFilter(double min, double max) { this.min = min; this.max = max; if (this.max < 0.0 || this.min > 1.0 || (this.min >= this.max)) { throw new IllegalArgumentException( "Max must be more than 0.0 and min must be more than 1.0, " + "and max must be at least equal to min; max was: " + this.max + "; min was " + this.min); } } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { return (toTest.getEvalution() >= min) && (toTest.getEvalution() <= max); } } /** * {@link StatSubmInfoFilter} that filters {@link StatisticalSubmissionInfo} -objects by their {@link StatisticalSubmissionInfo #getTimeOnTask()} value. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class TimeOnTaskFilter<A extends SubmissionInfo> implements StatSubmInfoFilter<A> { private final int maxTime; private final int minTime; /** * Constructs a new {@link TimeOnTaskFilter} matching certain interval of time-on-task values * * @param min * minimum matching time-on-task (seconds) * @param max * maximum matching time-on-task (seconds) */ public TimeOnTaskFilter(int min, int max) { this.minTime = min; this.maxTime = max; if (this.minTime >= this.maxTime) { throw new IllegalArgumentException( "Max must be at least equal to min; max was: " + this.maxTime + "; min was: " + this.minTime); } } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { return (toTest.getTimeOnTask() >= minTime) && (toTest.getTimeOnTask() <= maxTime); } } /** * <p> * {@link StatSubmInfoFilterConnector}-implementor that matches a {@link StatisticalSubmissionInfo}-object if all {@link StatSubmInfoFilter}s connected by * it match the {@link StatisticalSubmissionInfo}. * </p> * <p> * Matches ALL {@link StatisticalSubmissionInfo}-objects if the set of connected {@link StatSubmInfoFilter}s is EMPTY. * </p> * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class MatchAllFilter<A extends SubmissionInfo> implements StatSubmInfoFilterConnector<A> { private final List<StatSubmInfoFilter<A>> filters; /** * Constructs a new empty {@link MatchAllFilter}. */ public MatchAllFilter() { this(null); } /** * Constructs a new {@link MatchAllFilter} consisting of given filters. If given filters-list is null constructs an empty {@link MatchAllFilter}. * * @param filters * {@link List} of {@link StatSubmInfoFilter}s or null */ public MatchAllFilter(List<StatSubmInfoFilter<A>> filters) { if (filters != null) { this.filters = new ArrayList<StatSubmInfoFilter<A>>(filters); } else { this.filters = new ArrayList<StatSubmInfoFilter<A>>(); } } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { boolean res = true; for (StatSubmInfoFilter<A> aFilter : filters) { if (!aFilter.matches(toTest)) { res = false; break; } } return res; } @Override public List<StatSubmInfoFilter<A>> getConnectedFilters() { return filters; } @Override public void connectFilter(StatSubmInfoFilter<A> toConnect) { filters.add(toConnect); } @Override public void disconnectFilter(StatSubmInfoFilter<A> toDisconnect) { filters.remove(toDisconnect); } } /** * <p> * {@link StatSubmInfoFilterConnector}-implementor that matches a {@link StatisticalSubmissionInfo}-object if any of the {@link StatSubmInfoFilter}s * connected by it match the {@link StatisticalSubmissionInfo}. * </p> * <p> * NEVER matches a {@link StatisticalSubmissionInfo}-object if the set of connected {@link StatSubmInfoFilter}s is EMPTY. * </p> * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class MatchAnyFilter<A extends SubmissionInfo> implements StatSubmInfoFilterConnector<A> { private final List<StatSubmInfoFilter<A>> filters; /** * Constructs a new empty {@link MatchAnyFilter}. */ public MatchAnyFilter() { this(null); } /** * Constructs a new {@link MatchAnyFilter} containing all filters in given list or no filters if null is used. * * @param filters * {@link List} of {@link StatSubmInfoFilter}s */ public MatchAnyFilter(List<StatSubmInfoFilter<A>> filters) { if (filters != null) { this.filters = new ArrayList<StatSubmInfoFilter<A>>(filters); } else { this.filters = new ArrayList<StatSubmInfoFilter<A>>(); } } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { boolean res = false; for (StatSubmInfoFilter<A> aFilter : filters) { if (aFilter.matches(toTest)) { res = true; break; } } return res; } @Override public List<StatSubmInfoFilter<A>> getConnectedFilters() { return filters; } @Override public void connectFilter(StatSubmInfoFilter<A> toConnect) { filters.add(toConnect); } @Override public void disconnectFilter(StatSubmInfoFilter<A> toDisconnect) { filters.remove(toDisconnect); } } /** * {@link StatSubmInfoFilter}-implementor that matches a {@link StatisticalSubmissionInfo}-object if the {@link StatSubmInfoFilter} it is inverting does NOT * match that {@link StatisticalSubmissionInfo}. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class InvertedFilter<A extends SubmissionInfo> implements StatSubmInfoFilter<A> { private final StatSubmInfoFilter<A> toInvert; /** * Constructs a new {@link InvertedFilter} inverting given filter. * * @param toInvert * filter to invert. */ public InvertedFilter(StatSubmInfoFilter<A> toInvert) { this.toInvert = toInvert; } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { return !toInvert.matches(toTest); } /** * @return the underlying filter in its original (non-inverted) form */ public StatSubmInfoFilter<A> getUnderlyingFilter() { return toInvert; } } /** * Wrapper to implement {@link StatSubmInfoFilter} that only cares about the value of {@link StatisticalSubmissionInfo #getSubmissionData()}. * * @author Riku Haavisto * * @param <A> * accepted {@link SubmissionInfo}-type */ public static class BySubmMatcher<A extends SubmissionInfo> implements StatSubmInfoFilter<A> { private final SubmMatcher<A> submMatcher; /** * Constructs a new {@link BySubmMatcher} wrapping given {@link SubmMatcher} to implement {@link StatSubmInfoFilter}. * * @param submMatcher * {@link SubmMatcher} to wrap */ public BySubmMatcher(SubmMatcher<A> submMatcher) { this.submMatcher = submMatcher; } @Override public boolean matches(StatisticalSubmissionInfo<A> toTest) { return submMatcher.matches(toTest.getSubmissionData()); } } /** * Can be used to implement {@link StatSubmInfoFilter} that only cares about the value of {@link StatisticalSubmissionInfo #getSubmissionData()}. The * implementor must be wrapped with {@link BySubmMatcher}. * * @author Riku Haavisto * * @param <S> * accepted {@link SubmissionInfo}-type */ public interface SubmMatcher<S extends SubmissionInfo> { boolean matches(S toTest); } }