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