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