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}