001/*
002 * (C) Copyright 2014 Nuxeo SA (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.lang.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     * </p>
140     */
141    @Override
142    public Description getDescription() {
143        Map<String, Serializable> params = new HashMap<String, Serializable>();
144        if (minTime != null) {
145            params.put(PNAME_MINIMUM, new Date(minTime));
146            params.put(PNAME_MIN_INC, includingMin);
147        }
148        if (maxTime != null) {
149            params.put(PNAME_MAXIMUM, new Date(maxTime));
150            params.put(PNAME_MAX_INC, includingMax);
151        }
152        return new Description(DateIntervalConstraint.NAME, params);
153    }
154
155    public Long getMinTime() {
156        return minTime;
157    }
158
159    public Long getMaxTime() {
160        return maxTime;
161    }
162
163    public boolean isIncludingMin() {
164        return includingMin;
165    }
166
167    public boolean isIncludingMax() {
168        return includingMax;
169    }
170
171    @Override
172    public String getErrorMessage(Object invalidValue, Locale locale) {
173        // test whether there's a custom translation for this field constraint specific translation
174        // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxin ou
175        // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxin ou
176        // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxex ou
177        // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxex ou
178        // the expected key is label.schema.constraint.violation.[ConstraintName].minin ou
179        // the expected key is label.schema.constraint.violation.[ConstraintName].minex ou
180        // the expected key is label.schema.constraint.violation.[ConstraintName].maxin ou
181        // the expected key is label.schema.constraint.violation.[ConstraintName].maxex
182        // follow the AbstractConstraint behavior otherwise
183        Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG;
184        Object[] params;
185        String subKey = (minTime != null ? (includingMin ? "minin" : "minex") : "")
186                + (maxTime != null ? (includingMax ? "maxin" : "maxex") : "");
187        DateFormat format = DateFormat.getDateInstance(DateFormat.MEDIUM, computedLocale);
188        if (minTime != null && maxTime != null) {
189            String min = format.format(new Date(minTime));
190            String max = format.format(new Date(maxTime));
191            params = new Object[] { min, max };
192        } else if (minTime != null) {
193            String min = format.format(new Date(minTime));
194            params = new Object[] { min };
195        } else {
196            String max = format.format(new Date(maxTime));
197            params = new Object[] { max };
198        }
199        List<String> pathTokens = new ArrayList<String>();
200        pathTokens.add(MESSAGES_KEY);
201        pathTokens.add(DateIntervalConstraint.NAME);
202        pathTokens.add(subKey);
203        String key = StringUtils.join(pathTokens, '.');
204        String message = getMessageString(MESSAGES_BUNDLE, key, params, computedLocale);
205        if (message != null && !message.trim().isEmpty() && !key.equals(message)) {
206            // use a custom constraint message if there's one
207            return message;
208        } else {
209            // follow AbstractConstraint behavior otherwise
210            return super.getErrorMessage(invalidValue, computedLocale);
211        }
212    }
213
214    @Override
215    public int hashCode() {
216        final int prime = 31;
217        int result = 1;
218        result = prime * result + (includingMax ? 1231 : 1237);
219        result = prime * result + (includingMin ? 1231 : 1237);
220        result = prime * result + ((maxTime == null) ? 0 : maxTime.hashCode());
221        result = prime * result + ((minTime == null) ? 0 : minTime.hashCode());
222        return result;
223    }
224
225    @Override
226    public boolean equals(Object obj) {
227        if (this == obj) {
228            return true;
229        }
230        if (obj == null) {
231            return false;
232        }
233        if (getClass() != obj.getClass()) {
234            return false;
235        }
236        DateIntervalConstraint other = (DateIntervalConstraint) obj;
237        if (includingMax != other.includingMax) {
238            return false;
239        }
240        if (includingMin != other.includingMin) {
241            return false;
242        }
243        if (maxTime == null) {
244            if (other.maxTime != null) {
245                return false;
246            }
247        } else if (!maxTime.equals(other.maxTime)) {
248            return false;
249        }
250        if (minTime == null) {
251            if (other.minTime != null) {
252                return false;
253            }
254        } else if (!minTime.equals(other.minTime)) {
255            return false;
256        }
257        return true;
258    }
259
260}