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}