001/*
002 * #%L
003 * Netarchivesuite - harvester
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.harvester.webinterface;
024
025import java.text.ParseException;
026import java.text.SimpleDateFormat;
027import java.util.ArrayList;
028import java.util.Date;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Set;
032
033import javax.servlet.ServletRequest;
034
035import dk.netarkivet.common.CommonSettings;
036import dk.netarkivet.common.exceptions.ArgumentNotValid;
037import dk.netarkivet.common.utils.Settings;
038import dk.netarkivet.harvester.datamodel.JobStatus;
039
040/**
041 * Represents a query for a set of jobs. Filtering can be performed on:
042 * <ul>
043 * <li>Job Status (multiple)</li>
044 * <li>Harvest name (single)</li>
045 * <li>Job start date (day of month and year)</li>
046 * <li>Job end date (day of month and year)</li>
047 * </ul>
048 * <p>
049 * The semantics of the date filters is as follows:
050 * <ol>
051 * <li>If only a start date is specified, will fetch jobs whose start date is equal or posterior</li>
052 * <li>If only an end date is specified, will fetch jobs whose end date is equal or anterior</li>
053 * <li>If both are specified, will fetch jobs whose start and end date are equal or comprised between the specified
054 * bounds.</li>
055 * </ol>
056 * <p>
057 * The class enforces that end date is set at a date posterior to start date.
058 * <p>
059 * Additionally a sort order (applied to job IDs) can be set (ascending or descending), and the query can be limited to
060 * a certain row number and a start index.
061 */
062public class HarvestStatusQuery {
063
064    /** The String code to select all states. */
065    public static final String JOBSTATUS_ALL = "ALL";
066
067    /** The String code to select all harvests. */
068    public static final String HARVEST_NAME_ALL = "ALL";
069    /** String to check, if there is a wildcard in the harvestname. */
070    public static final String HARVEST_NAME_WILDCARD = "*";
071    /** Value used to define page size undefined. */
072    public static final long PAGE_SIZE_NONE = 0;
073    /** Value used to define date undefined. */
074    public static final long DATE_NONE = -1L;
075
076    /**
077     * Enum class defining the different sort-orders.
078     */
079    public static enum SORT_ORDER {
080        /** Ascending mode. From lowest to highest. */
081        ASC,
082        /** Descending mode. From highest to lowest. */
083        DESC;
084
085        /**
086         * Parse the given argument and return a sorting order.
087         *
088         * @param order a given sorting order as string
089         * @return a sorting order representing the given string.
090         */
091        public static SORT_ORDER parse(String order) {
092            for (SORT_ORDER o : values()) {
093                if (o.name().equals(order)) {
094                    return o;
095                }
096            }
097            return null;
098        }
099    }
100
101    /**
102     * Defines the UI fields and their default values.
103     */
104    public static enum UI_FIELD {
105        /** jobstatus with default status ALL. */
106        JOB_STATUS("ALL"),
107        /** JOB ID order. default is ascending. */
108        JOB_ID_ORDER("DESC"),
109        /** harvest name. default is ALL (i.e all harvests). */
110        HARVEST_NAME(HARVEST_NAME_ALL),
111        /** The harvest ID. No default. */
112        HARVEST_ID(""),
113        /** The harvest Run number. No default. */
114        HARVEST_RUN(""),
115        /** The harvest start date. No default. */
116        START_DATE(""),
117        /** The harvest end date. No default. */
118        END_DATE(""),
119        /** The job id range : list of job ids or range separated by commas, for instance:  2,4,8-14. No default. */
120        JOB_ID_RANGE(""),
121
122        
123        /**
124         * The number of results on each page. The default is read from the setting
125         * {@link CommonSettings#HARVEST_STATUS_DFT_PAGE_SIZE}.
126         */
127        PAGE_SIZE(Settings.get(CommonSettings.HARVEST_STATUS_DFT_PAGE_SIZE)),
128        /** The starting page. Default is 1. */
129        START_PAGE_INDEX("1"),
130        /** The number of Jobs to resubmit identified by ID. No default. */
131        RESUBMIT_JOB_IDS("");
132
133        /** The default value for this UI-field. */
134        private final String defaultValue;
135
136        /**
137         * Constructor for the UI_FIELD enum class.
138         *
139         * @param defaultValue the default value of the field.
140         */
141        UI_FIELD(String defaultValue) {
142            this.defaultValue = defaultValue;
143        }
144
145        /**
146         * Get the values stored in the request for this UI_FIELD.
147         *
148         * @param req the servlet request
149         * @return the values stored in the request for this UI_FIELD as a string array.
150         */
151        public String[] getValues(ServletRequest req) {
152            String[] values = req.getParameterValues(name());
153            if (values == null || values.length == 0) {
154                return new String[] {this.defaultValue};
155            }
156            return values;
157        }
158
159        /**
160         * Extracts the field's value from a servlet request. If the request does not define the paraeter's value, it is
161         * set to the default value.
162         *
163         * @param req a servlet request
164         * @return the field's value
165         */
166        public String getValue(ServletRequest req) {
167            String value = req.getParameter(name());
168            if (value == null || value.isEmpty()) {
169                return this.defaultValue;
170            }
171            return value;
172        }
173    }
174
175    /**
176     * The date format used by the calendar widget. It is actually the same format as the one represented by the
177     * DATE_FORMAT.
178     */
179    public static final String CALENDAR_UI_DATE_FORMAT = "%Y/%m/%d";
180
181    /** The date format used when returning dates as strings. */
182    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");
183
184    /** The job states selected in this query. */
185    private Set<JobStatus> jobStatuses = new HashSet<JobStatus>();
186    /** The harvestID. */
187    private Long harvestId;
188    /** The harvest run number. */
189    private Long harvestRunNumber;
190    /** The harvest name. */
191    private String harvestName;
192    /** The start date. */
193    private Date startDate;
194    /** The end date. */
195    private Date endDate;
196    /** The job id range : list of job ids or range separated by commas, for instance:  2,4,8-14. No default. */
197    private String jobIdRange;
198    private List<String> jobIdRangeList;
199    /** The sort order. */
200    private SORT_ORDER sortingOrder;
201    /** The page-size. */
202    private long pageSize;
203    /** The start page. */
204    private long startPageIndex;
205    /** Is the harvest name case sensitive. The default is yes. */
206    private boolean caseSensitiveHarvestName = true;
207
208    /**
209     * Builds a default query that will select all jobs.
210     */
211    public HarvestStatusQuery() {
212        jobIdRangeList = new ArrayList<String>();
213    }
214
215    /**
216     * Builds a default query that will find jobs for a given run of a harvest.
217     *
218     * @param harvestId A given harvestId
219     * @param harvestRunNumber a given harvestRunNumber
220     */
221    public HarvestStatusQuery(long harvestId, long harvestRunNumber) {
222        this.harvestId = harvestId;
223        this.harvestRunNumber = harvestRunNumber;
224        jobIdRangeList = new ArrayList<String>();
225    }
226
227    /**
228     * Builds a query from a servlet request. Unspecified fields are set to their default value.
229     *
230     * @param req a servlet request
231     */
232    public HarvestStatusQuery(ServletRequest req) {
233
234        String[] statuses = UI_FIELD.JOB_STATUS.getValues(req);
235        for (String s : statuses) {
236            if (JOBSTATUS_ALL.equals(s)) {
237                this.jobStatuses.clear();
238                break;
239            }
240            this.jobStatuses.add(JobStatus.parse(s));
241        }
242
243        this.harvestName = UI_FIELD.HARVEST_NAME.getValue(req);
244        if (HARVEST_NAME_ALL.equalsIgnoreCase(this.harvestName)) {
245            this.harvestName = "";
246        }
247
248        String harvestIdStr = UI_FIELD.HARVEST_ID.getValue(req);
249        try {
250            this.harvestId = Long.parseLong(harvestIdStr);
251        } catch (NumberFormatException e) {
252            this.harvestId = null;
253        }
254
255        String harvestRunStr = UI_FIELD.HARVEST_RUN.getValue(req);
256        try {
257            this.harvestRunNumber = Long.parseLong(harvestRunStr);
258        } catch (NumberFormatException e) {
259            this.harvestRunNumber = null;
260        }
261
262        String startDateStr = UI_FIELD.START_DATE.getValue(req);
263        if (!startDateStr.isEmpty()) {
264            try {
265                this.startDate = DATE_FORMAT.parse(startDateStr);
266            } catch (ParseException e) {
267                // Should never happen, date comes from a date selector UI
268                throw new ArgumentNotValid("Invalid date specification", e);
269            }
270        }
271
272        String endDateStr = UI_FIELD.END_DATE.getValue(req);
273        if (!endDateStr.isEmpty()) {
274            try {
275                this.endDate = DATE_FORMAT.parse(endDateStr);
276            } catch (ParseException e) {
277                // Should never happen, date comes from a date selector UI
278                throw new ArgumentNotValid("Invalid date specification", e);
279            }
280        }
281
282        if (startDate != null && endDate != null) {
283            if (endDate.before(this.startDate)) {
284                throw new ArgumentNotValid("End date is set after start date!");
285            }
286        }
287        
288        jobIdRangeList = new ArrayList<String>();
289        String jobIdRange = UI_FIELD.JOB_ID_RANGE.getValue(req);
290        if(!jobIdRange.isEmpty()) {
291                try {
292                        String[] splittedRange = jobIdRange.replaceAll("\\s+","").split(",");
293                        for(String s : splittedRange) {
294                                if(s.contains("-")) {
295                                        //if it's a range eg 11-27
296                                        String[] range = s.split("-");
297                                        if(range.length != 2) {
298                                                throw new ArgumentNotValid("Invalid Job IDs range (1-10 or 1,2,3)");
299                            }
300                                        //check if it's a number
301                                        Long.parseLong(range[0]);
302                                        Long.parseLong(range[1]);
303                                } else {
304                                        //check if it's a number
305                                        Long.parseLong(s);
306                                }
307                                jobIdRangeList.add(s);
308                        }
309                        this.jobIdRange = jobIdRange;
310                } catch (NumberFormatException e) {
311                    this.jobIdRange = null;
312                    throw new ArgumentNotValid("Job IDs must be digits", e);
313                }
314        }
315
316        String orderStr = UI_FIELD.JOB_ID_ORDER.getValue(req);
317        this.sortingOrder = SORT_ORDER.parse(orderStr);
318        if (this.sortingOrder == null) {
319            // Will not happen as value comes from a select
320            throw new ArgumentNotValid("Invalid sort order!");
321        }
322
323        String pageSizeStr = UI_FIELD.PAGE_SIZE.getValue(req);
324        try {
325            this.pageSize = Long.parseLong(pageSizeStr);
326        } catch (NumberFormatException e) {
327            throw new ArgumentNotValid("Invalid number!", e);
328        }
329
330        String startPageIndexStr = UI_FIELD.START_PAGE_INDEX.getValue(req);
331        try {
332            this.startPageIndex = Long.parseLong(startPageIndexStr);
333        } catch (NumberFormatException e) {
334            throw new ArgumentNotValid("Invalid number!", e);
335        }
336
337    }
338
339    /**
340     * @return the selected job states as an array.
341     */
342    public JobStatus[] getSelectedJobStatuses() {
343        return jobStatuses.toArray(new JobStatus[jobStatuses.size()]);
344    }
345
346    /**
347     * @return the selected job states as a set..
348     */
349    public Set<JobStatus> getSelectedJobStatusesAsSet() {
350        return jobStatuses;
351    }
352
353    /**
354     * @return the harvest name.
355     */
356    public String getHarvestName() {
357        if (harvestName == null) {
358            return "";
359        }
360        return harvestName;
361    }
362
363    /**
364     * Set the harvest name.
365     *
366     * @param harvestName The harvest name
367     */
368    public void setHarvestName(String harvestName) {
369        this.harvestName = harvestName;
370    }
371
372    /**
373     * @return the harvest ID.
374     */
375    public Long getHarvestId() {
376        return harvestId;
377    }
378
379    /**
380     * @return the harvest run number.
381     */
382    public Long getHarvestRunNumber() {
383        return harvestRunNumber;
384    }
385
386    /**
387     * @return the start date as milliseconds since Epoch or {@link HarvestStatusQuery#DATE_NONE} if start date is
388     * undefined
389     */
390    public long getStartDate() {
391        return (startDate == null ? DATE_NONE : startDate.getTime());
392    }
393
394    /**
395     * @return the end date as milliseconds since Epoch, or {@link HarvestStatusQuery#DATE_NONE} if end date is
396     * undefined
397     */
398    public long getEndDate() {
399        return (endDate == null ? DATE_NONE : endDate.getTime());
400    }
401
402    /**
403     * @return the job ids range as String
404     */
405    public String getJobIdRange() {
406        if(jobIdRange == null) {
407                return "";
408        }
409                return jobIdRange;
410        }
411
412    /**
413     * return only the ids or only the range
414     * if isRange is true : 2,3,5-9,14-18 -> 5-9,14-18
415     * if isRange is false : 2,3,5-9,14-18 -> 2,3
416     * @return the job ids range as List, only the ids or only the ranges
417     */
418    public List<String> getPartialJobIdRangeAsList(boolean isRange) {
419        List<String> list = new ArrayList<String>();
420        for(String s : jobIdRangeList) {
421                if(s.contains("-") == isRange) {
422                        list.add(s);
423                }
424        }
425                return list;
426        }
427
428        /**
429     * @return the start date as a string, or an empty string if start date is undefined
430     */
431    public String getStartDateAsString() {
432        if (startDate == null) {
433            return "";
434        }
435        return DATE_FORMAT.format(startDate);
436    }
437
438    /**
439     * @return the end date as a string, or an empty string if end date is undefined
440     */
441    public String getEndDateAsString() {
442        if (endDate == null) {
443            return "";
444        }
445        return DATE_FORMAT.format(endDate);
446    }
447
448    /**
449     * @return true, if the sorting order is Ascending, otherwise false.
450     */
451    public boolean isSortAscending() {
452        return SORT_ORDER.ASC.equals(sortingOrder);
453    }
454
455    /**
456     * @return the page size, i.e. the number of results on each page.
457     */
458    public long getPageSize() {
459        return pageSize;
460    }
461
462    /**
463     * Sets the page size.
464     *
465     * @param pageSize a number > 0.
466     */
467    public void setPageSize(long pageSize) {
468        ArgumentNotValid.checkNotNegative(pageSize, "pageSize");
469        this.pageSize = pageSize;
470    }
471
472    /**
473     * @return the start page
474     */
475    public long getStartPageIndex() {
476        return startPageIndex;
477    }
478
479    /**
480     * Define whether or not the harvest name is case sensitive.
481     *
482     * @param isHarvestNameCaseSensitive If true, harvestname is case sensitive, otherwise not.
483     */
484    public void setCaseSensitiveHarvestName(boolean isHarvestNameCaseSensitive) {
485        this.caseSensitiveHarvestName = isHarvestNameCaseSensitive;
486    }
487
488    /**
489     * @return true, if the harvest name is case sensitive, otherwise false
490     */
491    public boolean getCaseSensitiveHarvestName() {
492        return caseSensitiveHarvestName;
493    }
494
495    /**
496     * Set the selected states in the query.
497     *
498     * @param chosenStates the set of selected states.
499     */
500    public void setJobStatus(Set<JobStatus> chosenStates) {
501        this.jobStatuses = chosenStates;
502    }
503}