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.math.BigDecimal; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027 028import org.apache.commons.lang.StringUtils; 029 030/** 031 * This constraint ensures a numeric is in an interval. 032 * <p> 033 * This constraint can validate any {@link Number}. 034 * </p> 035 * 036 * @since 7.1 037 */ 038public class NumericIntervalConstraint extends AbstractConstraint { 039 040 private static final long serialVersionUID = 3630463971175189087L; 041 042 private static final String NAME = "NumericIntervalConstraint"; 043 044 private static final String PNAME_MINIMUM = "Minimum"; 045 046 private static final String PNAME_MAXIMUM = "Maximum"; 047 048 private static final String PNAME_MIN_INC = "MinimumInclusive"; 049 050 private static final String PNAME_MAX_INC = "MaximumInclusive"; 051 052 private final BigDecimal min; 053 054 private final BigDecimal max; 055 056 private final boolean includingMin; 057 058 private final boolean includingMax; 059 060 /** 061 * Use null value to disable a bound. 062 * <p> 063 * Bounds could be any object having toString representating a number. 064 * </p> 065 * 066 * @param min The lower bound of the interval 067 * @param includingMin true if the lower bound is included in the interval 068 * @param max The upper bound of the interval 069 * @param includingMax true if the upper bound is included in the interval 070 */ 071 public NumericIntervalConstraint(Object min, boolean includingMin, Object max, boolean includingMax) { 072 this.min = ConstraintUtils.objectToBigDecimal(min); 073 this.includingMin = includingMin; 074 this.max = ConstraintUtils.objectToBigDecimal(max); 075 this.includingMax = includingMax; 076 } 077 078 @Override 079 public boolean validate(Object object) { 080 BigDecimal val = ConstraintUtils.objectToBigDecimal(object); 081 if (val == null) { 082 return true; 083 } 084 if (min != null) { 085 int test = min.compareTo(val); 086 if (test > 0) { 087 return false; 088 } 089 if (!includingMin && test == 0) { 090 return false; 091 } 092 } 093 if (max != null) { 094 int test = max.compareTo(val); 095 if (test < 0) { 096 return false; 097 } 098 if (!includingMax && test == 0) { 099 return false; 100 } 101 } 102 return true; 103 } 104 105 /** 106 * Here, value is : <br> 107 * name = {@value #NAME} <br> 108 * parameters = 109 * <ul> 110 * <li>{@value #PNAME_MINIMUM} : -125.87 // only if bounded</li> 111 * <li>{@value #PNAME_MIN_INC} : true // only if bounded</li> 112 * <li>{@value #PNAME_MAXIMUM} : 232 // only if bounded</li> 113 * <li>{@value #PNAME_MAX_INC} : false // only if bounded</li> 114 * </ul> 115 */ 116 @Override 117 public Description getDescription() { 118 Map<String, Serializable> params = new HashMap<String, Serializable>(); 119 if (min != null) { 120 params.put(PNAME_MINIMUM, min); 121 params.put(PNAME_MIN_INC, includingMin); 122 } 123 if (max != null) { 124 params.put(PNAME_MAXIMUM, max); 125 params.put(PNAME_MAX_INC, includingMax); 126 } 127 return new Description(NumericIntervalConstraint.NAME, params); 128 } 129 130 public BigDecimal getMin() { 131 return min; 132 } 133 134 public BigDecimal getMax() { 135 return max; 136 } 137 138 public boolean isIncludingMin() { 139 return includingMin; 140 } 141 142 public boolean isIncludingMax() { 143 return includingMax; 144 } 145 146 @Override 147 public String getErrorMessage(Object invalidValue, Locale locale) { 148 // test whether there's a custom translation for this field constraint specific translation 149 // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxin ou 150 // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxin ou 151 // the expected key is label.schema.constraint.violation.[ConstraintName].mininmaxex ou 152 // the expected key is label.schema.constraint.violation.[ConstraintName].minexmaxex ou 153 // the expected key is label.schema.constraint.violation.[ConstraintName].minin ou 154 // the expected key is label.schema.constraint.violation.[ConstraintName].minex ou 155 // the expected key is label.schema.constraint.violation.[ConstraintName].maxin ou 156 // the expected key is label.schema.constraint.violation.[ConstraintName].maxex 157 // follow the AbstractConstraint behavior otherwise 158 Object[] params; 159 String subKey = (min != null ? (includingMin ? "minin" : "minex") : "") 160 + (max != null ? (includingMax ? "maxin" : "maxex") : ""); 161 if (min != null && max != null) { 162 params = new Object[] { min, max }; 163 } else if (min != null) { 164 params = new Object[] { min }; 165 } else { 166 params = new Object[] { max }; 167 } 168 List<String> pathTokens = new ArrayList<String>(); 169 pathTokens.add(MESSAGES_KEY); 170 pathTokens.add(NumericIntervalConstraint.NAME); 171 pathTokens.add(subKey); 172 String key = StringUtils.join(pathTokens, '.'); 173 Locale computedLocale = locale != null ? locale : Constraint.MESSAGES_DEFAULT_LANG; 174 String message = getMessageString(MESSAGES_BUNDLE, key, params, computedLocale); 175 if (message != null && !message.trim().isEmpty() && !key.equals(message)) { 176 // use a custom constraint message if there's one 177 return message; 178 } else { 179 // follow AbstractConstraint behavior otherwise 180 return super.getErrorMessage(invalidValue, computedLocale); 181 } 182 } 183 184 @Override 185 public int hashCode() { 186 final int prime = 31; 187 int result = 1; 188 result = prime * result + (includingMax ? 1231 : 1237); 189 result = prime * result + (includingMin ? 1231 : 1237); 190 result = prime * result + ((max == null) ? 0 : max.hashCode()); 191 result = prime * result + ((min == null) ? 0 : min.hashCode()); 192 return result; 193 } 194 195 @Override 196 public boolean equals(Object obj) { 197 if (this == obj) { 198 return true; 199 } 200 if (obj == null) { 201 return false; 202 } 203 if (getClass() != obj.getClass()) { 204 return false; 205 } 206 NumericIntervalConstraint other = (NumericIntervalConstraint) obj; 207 if (includingMax != other.includingMax) { 208 return false; 209 } 210 if (includingMin != other.includingMin) { 211 return false; 212 } 213 if (max == null) { 214 if (other.max != null) { 215 return false; 216 } 217 } else if (!max.equals(other.max)) { 218 return false; 219 } 220 if (min == null) { 221 if (other.min != null) { 222 return false; 223 } 224 } else if (!min.equals(other.min)) { 225 return false; 226 } 227 return true; 228 } 229 230}