001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nicolas Chapurlat <nchapurlat@nuxeo.com>
016 */
017
018package org.nuxeo.ecm.core.schema.types.constraints;
019
020import java.io.Serializable;
021import java.text.DateFormat;
022import java.util.ArrayList;
023import java.util.Calendar;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029
030import org.apache.commons.lang.StringUtils;
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034/**
035 * This constraint ensures a date is in an interval.
036 * <p>
037 * This constraint can validate any {@link Date} or {@link Calendar}. This constraint also support {@link Number} types
038 * whose long value is recognised as number of milliseconds since January 1, 1970, 00:00:00 GMT. The constraint finally
039 * supports String having YYYY-MM-DD format.
040 * </p>
041 *
042 * @since 7.1
043 */
044public class DateIntervalConstraint extends AbstractConstraint {
045
046    private static final long serialVersionUID = 3630463971175189087L;
047
048    private static final Log log = LogFactory.getLog(DateIntervalConstraint.class);
049
050    private static final String NAME = "DateIntervalConstraint";
051
052    private static final String PNAME_MINIMUM = "Minimum";
053
054    private static final String PNAME_MAXIMUM = "Maximum";
055
056    private static final String PNAME_MIN_INC = "MinimumInclusive";
057
058    private static final String PNAME_MAX_INC = "MaximumInclusive";
059
060    private final Long minTime;
061
062    private final Long maxTime;
063
064    private final boolean includingMin;
065
066    private final boolean includingMax;
067
068    /**
069     * Use null value to disable a bound.
070     * <p>
071     * Bounds could be any {@link Date} or {@link Calendar}. Bounds also support {@link Number} types whose long value
072     * is recognised as number of milliseconds since January 1, 1970, 00:00:00 GMT. Bounds finally supports String
073     * having YYYY-MM-DD format.
074     * </p>
075     * <p>
076     * Invalid bound (wrong format) would be ignored with log warning.
077     * </p>
078     *
079     * @param minDate The lower bound of the interval
080     * @param includingMin true if the lower bound is included in the interval
081     * @param maxDate The upper bound of the interval
082     * @param includingMax true if the upper bound is included in the interval
083     */
084    public DateIntervalConstraint(Object minDate, boolean includingMin, Object maxDate, boolean includingMax) {
085        minTime = ConstraintUtils.objectToTimeMillis(minDate);
086        this.includingMin = includingMin;
087        maxTime = ConstraintUtils.objectToTimeMillis(maxDate);
088        this.includingMax = includingMax;
089        if (minTime != null && maxTime != null && minTime > maxTime) {
090            log.warn("lower bound (" + minDate + ") is greater than upper bound (" + maxDate
091                    + "). No dates could be valid.");
092        }
093        if ((minTime == null && minDate != null) || (maxTime == null && maxDate != null)) {
094            log.warn("some bound was ignored due to invalid date format (supported format is "
095                    + ConstraintUtils.DATE_FORMAT + " (min = " + minDate + " - max = " + maxDate + ")");
096        }
097    }
098
099    @Override
100    public boolean validate(Object object) {
101        if (object == null) {
102            return true;
103        }
104        Long timeValue = ConstraintUtils.objectToTimeMillis(object);
105        if (timeValue == null) {
106            return false;
107        }
108        if (minTime != null) {
109            if (timeValue < minTime.longValue()) {
110                return false;
111            }
112            if (!includingMin && timeValue == minTime.longValue()) {
113                return false;
114            }
115        }
116        if (maxTime != null) {
117            if (timeValue > maxTime.longValue()) {
118                return false;
119            }
120            if (!includingMax && timeValue == maxTime.longValue()) {
121                return false;
122            }
123        }
124        return true;
125    }
126
127    /**
128     * Here, value is : <br>
129     * name = {@value #NAME}. <br>
130     * parameters =
131     * <ul>
132     * <li>{@value #PNAME_MINIMUM} : 2014-11-05 // only if bounded</li>
133     * <li>{@value #PNAME_MIN_INC} : true // only if bounded</li>
134     * <li>{@value #PNAME_MAXIMUM} : 2014-11-25 // only if bounded</li>
135     * <li>{@value #PNAME_MAX_INC} : false // only if bounded</li>
136     * </ul>
137     * </p>
138     */
139    @Override
140    public Description getDescription() {
141        Map<String, Serializable> params = new HashMap<String, Serializable>();
142        if (minTime != null) {
143            params.put(PNAME_MINIMUM, new Date(minTime));
144            params.put(PNAME_MIN_INC, includingMin);
145        }
146        if (maxTime != null) {
147            params.put(PNAME_MAXIMUM, new Date(maxTime));
148            params.put(PNAME_MAX_INC, includingMax);
149        }
150        return new Description(DateIntervalConstraint.NAME, params);
151    }
152
153    public Long getMinTime() {
154        return minTime;
155    }
156
157    public Long getMaxTime() {
158        return maxTime;
159    }
160
161    public boolean isIncludingMin() {
162        return includingMin;
163    }
164
165    public boolean isIncludingMax() {
166        return includingMax;
167    }
168
169    @Override
170    public String getErrorMessage(Object invalidValue, Locale locale) {
171        // test whether there's a custom translation for this field constraint specific translation
172        // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxin ou
173        // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxin ou
174        // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxex ou
175        // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxex ou
176        // the expected key is label.schema.constraint.violation.[ConstraintName].minin ou
177        // the expected key is label.schema.constraint.violation.[ConstraintName].minex ou
178        // the expected key is label.schema.constraint.violation.[ConstraintName].maxin ou
179        // the expected key is label.schema.constraint.violation.[ConstraintName].maxex
180        // follow the AbstractConstraint behavior otherwise
181        Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG;
182        Object[] params;
183        String subKey = (minTime != null ? (includingMin ? "minin" : "minex") : "")
184                + (maxTime != null ? (includingMax ? "maxin" : "maxex") : "");
185        DateFormat format = DateFormat.getDateInstance(DateFormat.MEDIUM, computedLocale);
186        if (minTime != null && maxTime != null) {
187            String min = format.format(new Date(minTime));
188            String max = format.format(new Date(maxTime));
189            params = new Object[] { min, max };
190        } else if (minTime != null) {
191            String min = format.format(new Date(minTime));
192            params = new Object[] { min };
193        } else {
194            String max = format.format(new Date(maxTime));
195            params = new Object[] { max };
196        }
197        List<String> pathTokens = new ArrayList<String>();
198        pathTokens.add(MESSAGES_KEY);
199        pathTokens.add(DateIntervalConstraint.NAME);
200        pathTokens.add(subKey);
201        String key = StringUtils.join(pathTokens, '.');
202        String message = getMessageString(MESSAGES_BUNDLE, key, params, computedLocale);
203        if (message != null && !message.trim().isEmpty() && !key.equals(message)) {
204            // use a custom constraint message if there's one
205            return message;
206        } else {
207            // follow AbstractConstraint behavior otherwise
208            return super.getErrorMessage(invalidValue, computedLocale);
209        }
210    }
211
212    @Override
213    public int hashCode() {
214        final int prime = 31;
215        int result = 1;
216        result = prime * result + (includingMax ? 1231 : 1237);
217        result = prime * result + (includingMin ? 1231 : 1237);
218        result = prime * result + ((maxTime == null) ? 0 : maxTime.hashCode());
219        result = prime * result + ((minTime == null) ? 0 : minTime.hashCode());
220        return result;
221    }
222
223    @Override
224    public boolean equals(Object obj) {
225        if (this == obj) {
226            return true;
227        }
228        if (obj == null) {
229            return false;
230        }
231        if (getClass() != obj.getClass()) {
232            return false;
233        }
234        DateIntervalConstraint other = (DateIntervalConstraint) obj;
235        if (includingMax != other.includingMax) {
236            return false;
237        }
238        if (includingMin != other.includingMin) {
239            return false;
240        }
241        if (maxTime == null) {
242            if (other.maxTime != null) {
243                return false;
244            }
245        } else if (!maxTime.equals(other.maxTime)) {
246            return false;
247        }
248        if (minTime == null) {
249            if (other.minTime != null) {
250                return false;
251            }
252        } else if (!minTime.equals(other.minTime)) {
253            return false;
254        }
255        return true;
256    }
257
258}