001/* 002 * (C) Copyright 2015 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 * Anahide Tchertchian 018 */ 019package org.nuxeo.ecm.platform.ui.web.validator; 020 021import java.util.LinkedHashSet; 022import java.util.List; 023import java.util.Locale; 024import java.util.Set; 025 026import javax.el.ValueExpression; 027import javax.el.ValueReference; 028import javax.faces.application.FacesMessage; 029import javax.faces.component.PartialStateHolder; 030import javax.faces.component.UIComponent; 031import javax.faces.context.FacesContext; 032import javax.faces.validator.Validator; 033import javax.faces.validator.ValidatorException; 034 035import org.apache.commons.lang.StringUtils; 036import org.apache.commons.logging.Log; 037import org.apache.commons.logging.LogFactory; 038import org.nuxeo.ecm.core.api.DocumentModel; 039import org.nuxeo.ecm.core.api.model.Property; 040import org.nuxeo.ecm.core.api.validation.ConstraintViolation; 041import org.nuxeo.ecm.core.api.validation.DocumentValidationReport; 042import org.nuxeo.ecm.core.api.validation.DocumentValidationService; 043import org.nuxeo.ecm.core.schema.SchemaManager; 044import org.nuxeo.ecm.core.schema.types.Field; 045import org.nuxeo.ecm.platform.el.DocumentPropertyContext; 046import org.nuxeo.ecm.platform.ui.web.model.ProtectedEditableModel; 047import org.nuxeo.ecm.platform.ui.web.validator.ValueExpressionAnalyzer.ListItemMapper; 048import org.nuxeo.runtime.api.Framework; 049 050/** 051 * JSF validator for {@link DocumentModel} field constraints. 052 * 053 * @since 7.2 054 */ 055public class DocumentConstraintValidator implements Validator, PartialStateHolder { 056 057 private static final Log log = LogFactory.getLog(DocumentConstraintValidator.class); 058 059 public static final String VALIDATOR_ID = "DocumentConstraintValidator"; 060 061 private boolean transientValue = false; 062 063 private boolean initialState; 064 065 protected Boolean handleSubProperties; 066 067 @Override 068 public void validate(FacesContext context, UIComponent component, Object value) throws ValidatorException { 069 if (context == null) { 070 throw new NullPointerException(); 071 } 072 if (component == null) { 073 throw new NullPointerException(); 074 } 075 ValueExpression ve = component.getValueExpression("value"); 076 if (ve == null) { 077 return; 078 } 079 080 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(ve); 081 ValueReference vref = expressionAnalyzer.getReference(context.getELContext()); 082 083 if (log.isDebugEnabled()) { 084 log.debug(String.format("Validating value '%s' for expression '%s', base=%s, prop=%s", value, 085 ve.getExpressionString(), vref.getBase(), vref.getProperty())); 086 } 087 088 if (isResolvable(vref, ve)) { 089 List<ConstraintViolation> violations = doValidate(context, vref, ve, value); 090 if (violations != null && !violations.isEmpty()) { 091 Locale locale = context.getViewRoot().getLocale(); 092 if (violations.size() == 1) { 093 ConstraintViolation v = violations.iterator().next(); 094 String msg = v.getMessage(locale); 095 throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg)); 096 } else { 097 Set<FacesMessage> messages = new LinkedHashSet<FacesMessage>(violations.size()); 098 for (ConstraintViolation v : violations) { 099 String msg = v.getMessage(locale); 100 messages.add(new FacesMessage(FacesMessage.SEVERITY_ERROR, msg, msg)); 101 } 102 throw new ValidatorException(messages); 103 } 104 } 105 } 106 } 107 108 @SuppressWarnings("rawtypes") 109 private boolean isResolvable(ValueReference ref, ValueExpression ve) { 110 if (ve == null || ref == null) { 111 return false; 112 } 113 Object base = ref.getBase(); 114 if (base != null) { 115 Class baseClass = base.getClass(); 116 if (baseClass != null) { 117 if (DocumentPropertyContext.class.isAssignableFrom(baseClass) 118 || (Property.class.isAssignableFrom(baseClass)) 119 || (ProtectedEditableModel.class.isAssignableFrom(baseClass)) 120 || (ListItemMapper.class.isAssignableFrom(baseClass))) { 121 return true; 122 } 123 } 124 } 125 if (log.isDebugEnabled()) { 126 log.debug(String.format("NOT validating %s, base=%s, prop=%s", ve.getExpressionString(), base, 127 ref.getProperty())); 128 } 129 return false; 130 } 131 132 protected List<ConstraintViolation> doValidate(FacesContext context, ValueReference vref, ValueExpression e, 133 Object value) { 134 DocumentValidationService s = Framework.getService(DocumentValidationService.class); 135 DocumentValidationReport report = null; 136 XPathAndField field = resolveField(context, vref, e); 137 if (field != null) { 138 boolean validateSubs = getHandleSubProperties().booleanValue(); 139 // use the xpath to validate the field 140 // this allow to get the custom message defined for field if there's error 141 report = s.validate(field.xpath, value, validateSubs); 142 if (log.isDebugEnabled()) { 143 log.debug(String.format("VALIDATED value '%s' for expression '%s', base=%s, prop=%s", value, 144 e.getExpressionString(), vref.getBase(), vref.getProperty())); 145 } 146 } else { 147 if (log.isDebugEnabled()) { 148 log.debug(String.format("NOT Validating value '%s' for expression '%s', base=%s, prop=%s", value, 149 e.getExpressionString(), vref.getBase(), vref.getProperty())); 150 } 151 } 152 153 if (report != null && report.hasError()) { 154 return report.asList(); 155 } 156 157 return null; 158 } 159 160 private class XPathAndField { 161 162 private Field field; 163 164 private String xpath; 165 166 public XPathAndField(Field field, String xpath) { 167 super(); 168 this.field = field; 169 this.xpath = xpath; 170 } 171 172 } 173 174 protected XPathAndField resolveField(FacesContext context, ValueReference vref, ValueExpression ve) { 175 Object base = vref.getBase(); 176 Object propObj = vref.getProperty(); 177 if (propObj != null && !(propObj instanceof String)) { 178 // ignore cases where prop would not be a String 179 return null; 180 } 181 String xpath = null; 182 Field field = null; 183 String prop = (String) propObj; 184 Class<?> baseClass = base.getClass(); 185 if (DocumentPropertyContext.class.isAssignableFrom(baseClass)) { 186 DocumentPropertyContext dc = (DocumentPropertyContext) base; 187 xpath = dc.getSchema() + ":" + prop; 188 field = getField(xpath); 189 } else if (Property.class.isAssignableFrom(baseClass)) { 190 xpath = ((Property) base).getPath() + "/" + prop; 191 field = getField(((Property) base).getField(), prop); 192 } else if (ProtectedEditableModel.class.isAssignableFrom(baseClass)) { 193 ProtectedEditableModel model = (ProtectedEditableModel) base; 194 ValueExpression listVe = model.getBinding(); 195 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe); 196 ValueReference listRef = expressionAnalyzer.getReference(context.getELContext()); 197 if (isResolvable(listRef, listVe)) { 198 XPathAndField parentField = resolveField(context, listRef, listVe); 199 if (parentField != null) { 200 field = getField(parentField.field, "*"); 201 if (parentField.xpath == null) { 202 xpath = field.getName().getLocalName(); 203 } else { 204 xpath = parentField.xpath + "/" + field.getName().getLocalName(); 205 } 206 } 207 } 208 } else if (ListItemMapper.class.isAssignableFrom(baseClass)) { 209 ListItemMapper mapper = (ListItemMapper) base; 210 ProtectedEditableModel model = mapper.getModel(); 211 ValueExpression listVe; 212 if (model.getParent() != null) { 213 // move one level up to resolve parent list binding 214 listVe = model.getParent().getBinding(); 215 } else { 216 listVe = model.getBinding(); 217 } 218 ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe); 219 ValueReference listRef = expressionAnalyzer.getReference(context.getELContext()); 220 if (isResolvable(listRef, listVe)) { 221 XPathAndField parentField = resolveField(context, listRef, listVe); 222 if (parentField != null) { 223 field = getField(parentField.field, prop); 224 if (field == null || field.getName() == null) { 225 // it should not happen but still, just in case 226 return null; 227 } 228 if (parentField.xpath == null) { 229 xpath = field.getName().getLocalName(); 230 } else { 231 xpath = parentField.xpath + "/" + field.getName().getLocalName(); 232 } 233 } 234 } 235 } else { 236 log.error(String.format("Cannot validate expression '%s, base=%s'", ve.getExpressionString(), base)); 237 } 238 // cleanup / on begin or at end 239 if (xpath != null) { 240 xpath = StringUtils.strip(xpath, "/"); 241 } else if (field == null && xpath == null) { 242 return null; 243 } 244 return new XPathAndField(field, xpath); 245 } 246 247 protected Field getField(Field field, String subName) { 248 SchemaManager tm = Framework.getService(SchemaManager.class); 249 return tm.getField(field, subName); 250 } 251 252 protected Field getField(String propertyName) { 253 SchemaManager tm = Framework.getService(SchemaManager.class); 254 return tm.getField(propertyName); 255 } 256 257 public Boolean getHandleSubProperties() { 258 return handleSubProperties != null ? handleSubProperties : Boolean.TRUE; 259 } 260 261 public void setHandleSubProperties(Boolean handleSubProperties) { 262 clearInitialState(); 263 this.handleSubProperties = handleSubProperties; 264 } 265 266 @Override 267 public Object saveState(FacesContext context) { 268 if (context == null) { 269 throw new NullPointerException(); 270 } 271 if (!initialStateMarked()) { 272 Object values[] = new Object[1]; 273 values[0] = handleSubProperties; 274 return (values); 275 } 276 return null; 277 } 278 279 @Override 280 public void restoreState(FacesContext context, Object state) { 281 if (context == null) { 282 throw new NullPointerException(); 283 } 284 if (state != null) { 285 Object values[] = (Object[]) state; 286 handleSubProperties = (Boolean) values[0]; 287 } 288 } 289 290 @Override 291 public boolean isTransient() { 292 return transientValue; 293 } 294 295 @Override 296 public void setTransient(boolean newTransientValue) { 297 this.transientValue = newTransientValue; 298 } 299 300 @Override 301 public void markInitialState() { 302 initialState = true; 303 } 304 305 @Override 306 public boolean initialStateMarked() { 307 return initialState; 308 } 309 310 @Override 311 public void clearInitialState() { 312 initialState = false; 313 } 314 315}