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}