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