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}