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        XPathAndField field = resolveField(context, vref, e);
134        if (field != null) {
135            boolean validateSubs = getHandleSubProperties().booleanValue();
136            // use the xpath to validate the field
137            // this allow to get the custom message defined for field if there's error
138            report = s.validate(field.xpath, value, validateSubs);
139            if (log.isDebugEnabled()) {
140                log.debug(String.format("VALIDATED  value '%s' for expression '%s', base=%s, prop=%s", value,
141                        e.getExpressionString(), vref.getBase(), vref.getProperty()));
142            }
143        } else {
144            if (log.isDebugEnabled()) {
145                log.debug(String.format("NOT Validating  value '%s' for expression '%s', base=%s, prop=%s", value,
146                        e.getExpressionString(), vref.getBase(), vref.getProperty()));
147            }
148        }
149
150        if (report != null && report.hasError()) {
151            return report.asList();
152        }
153
154        return null;
155    }
156
157    private class XPathAndField {
158
159        private Field field;
160
161        private String xpath;
162
163        public XPathAndField(Field field, String xpath) {
164            super();
165            this.field = field;
166            this.xpath = xpath;
167        }
168
169    }
170
171    protected XPathAndField resolveField(FacesContext context, ValueReference vref, ValueExpression ve) {
172        Object base = vref.getBase();
173        Object propObj = vref.getProperty();
174        if (propObj != null && !(propObj instanceof String)) {
175            // ignore cases where prop would not be a String
176            return null;
177        }
178        String xpath = null;
179        Field field = null;
180        String prop = (String) propObj;
181        Class<?> baseClass = base.getClass();
182        if (DocumentPropertyContext.class.isAssignableFrom(baseClass)) {
183            DocumentPropertyContext dc = (DocumentPropertyContext) base;
184            xpath = String.format("%s:%s", dc.getSchema(), prop);
185            field = getField(xpath);
186        } else if (Property.class.isAssignableFrom(baseClass)) {
187            xpath = ((Property) base).getPath() + "/" + prop;
188            field = getField(((Property) base).getField(), prop);
189        } else if (ProtectedEditableModel.class.isAssignableFrom(baseClass)) {
190            ProtectedEditableModel model = (ProtectedEditableModel) base;
191            ValueExpression listVe = model.getBinding();
192            ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe);
193            ValueReference listRef = expressionAnalyzer.getReference(context.getELContext());
194            if (isResolvable(listRef, listVe)) {
195                XPathAndField parentField = resolveField(context, listRef, listVe);
196                if (parentField != null) {
197                    field = getField(parentField.field, "*");
198                    if (parentField.xpath == null) {
199                        xpath = field.getName().getLocalName();
200                    } else {
201                        xpath = parentField.xpath + "/" + field.getName().getLocalName();
202                    }
203                }
204            }
205        } else if (ListItemMapper.class.isAssignableFrom(baseClass)) {
206            ListItemMapper mapper = (ListItemMapper) base;
207            ProtectedEditableModel model = mapper.getModel();
208            ValueExpression listVe;
209            if (model.getParent() != null) {
210                // move one level up to resolve parent list binding
211                listVe = model.getParent().getBinding();
212            } else {
213                listVe = model.getBinding();
214            }
215            ValueExpressionAnalyzer expressionAnalyzer = new ValueExpressionAnalyzer(listVe);
216            ValueReference listRef = expressionAnalyzer.getReference(context.getELContext());
217            if (isResolvable(listRef, listVe)) {
218                XPathAndField parentField = resolveField(context, listRef, listVe);
219                if (parentField != null) {
220                    field = getField(parentField.field, prop);
221                    if (parentField.xpath == null) {
222                        xpath = field.getName().getLocalName();
223                    } else {
224                        xpath = parentField.xpath + "/" + field.getName().getLocalName();
225                    }
226                }
227            }
228        } else {
229            log.error(String.format("Cannot validate expression '%s, base=%s'", ve.getExpressionString(), base));
230        }
231        // cleanup / on begin or at end
232        if (xpath != null) {
233            xpath = xpath.replaceAll("(^\\/)|(\\/$)", "");
234        }
235        return new XPathAndField(field, xpath.replaceAll("(^\\\\/)|(\\\\/$)", ""));
236    }
237
238    protected Field getField(Field field, String subName) {
239        SchemaManager tm = Framework.getService(SchemaManager.class);
240        return tm.getField(field, subName);
241    }
242
243    protected Field getField(String propertyName) {
244        SchemaManager tm = Framework.getService(SchemaManager.class);
245        return tm.getField(propertyName);
246    }
247
248    public Boolean getHandleSubProperties() {
249        return handleSubProperties != null ? handleSubProperties : Boolean.TRUE;
250    }
251
252    public void setHandleSubProperties(Boolean handleSubProperties) {
253        clearInitialState();
254        this.handleSubProperties = handleSubProperties;
255    }
256
257    @Override
258    public Object saveState(FacesContext context) {
259        if (context == null) {
260            throw new NullPointerException();
261        }
262        if (!initialStateMarked()) {
263            Object values[] = new Object[1];
264            values[0] = handleSubProperties;
265            return (values);
266        }
267        return null;
268    }
269
270    @Override
271    public void restoreState(FacesContext context, Object state) {
272        if (context == null) {
273            throw new NullPointerException();
274        }
275        if (state != null) {
276            Object values[] = (Object[]) state;
277            handleSubProperties = (Boolean) values[0];
278        }
279    }
280
281    @Override
282    public boolean isTransient() {
283        return transientValue;
284    }
285
286    @Override
287    public void setTransient(boolean newTransientValue) {
288        this.transientValue = newTransientValue;
289    }
290
291    @Override
292    public void markInitialState() {
293        initialState = true;
294    }
295
296    @Override
297    public boolean initialStateMarked() {
298        return initialState;
299    }
300
301    @Override
302    public void clearInitialState() {
303        initialState = false;
304    }
305
306}