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}