001/*
002 * (C) Copyright 2006-2007 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 *     Nuxeo - initial API and implementation
018 *
019 * $Id: DocumentModelResolver.java 23589 2007-08-08 16:50:40Z fguillaume $
020 */
021
022package org.nuxeo.ecm.platform.el;
023
024import java.io.Serializable;
025import java.util.List;
026import java.util.Map;
027import java.util.stream.Collectors;
028
029import javax.el.BeanELResolver;
030import javax.el.ELContext;
031import javax.el.PropertyNotFoundException;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035import org.nuxeo.ecm.core.api.Blob;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.PropertyException;
038import org.nuxeo.ecm.core.api.model.Property;
039import org.nuxeo.ecm.core.api.model.impl.ArrayProperty;
040import org.nuxeo.ecm.core.api.model.impl.ComplexProperty;
041import org.nuxeo.ecm.core.api.model.impl.ListProperty;
042
043/**
044 * Resolves expressions for the {@link DocumentModel} framework.
045 * <p>
046 * To specify a property on a document mode, the following syntax is available:
047 * <code>myDocumentModel.dublincore.title</code> where 'dublincore' is the schema name and 'title' is the field name. It
048 * can be used to get or set the document title: {@code <h:outputText value="# {currentDocument.dublincore.title}" />}
049 * or {@code <h:inputText value="# {currentDocument.dublincore.title}" />}.
050 * <p>
051 * Simple document properties are get/set directly: for instance, the above expression will return a String value on
052 * get, and set this String on the document for set. Complex properties (maps and lists) are get/set through the
053 * {@link Property} object controlling their value: on get, sub properties will be resolved at the next iteration, and
054 * on set, they will be set on the property instance so the document model is aware of the change.
055 *
056 * @author <a href="mailto:rcaraghin@nuxeo.com">Razvan Caraghin</a>
057 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
058 */
059public class DocumentModelResolver extends BeanELResolver {
060
061    private static final Log log = LogFactory.getLog(DocumentModelResolver.class);
062
063    // XXX AT: see if getFeatureDescriptor needs to be overloaded to return
064    // datamodels descriptors.
065
066    @Override
067    public Class<?> getType(ELContext context, Object base, Object property) {
068        Class<?> type = null;
069        if (base instanceof DocumentModel) {
070            try {
071                type = super.getType(context, base, property);
072            } catch (PropertyNotFoundException e) {
073                type = DocumentPropertyContext.class;
074                context.setPropertyResolved(true);
075            }
076        } else if (base instanceof DocumentPropertyContext || base instanceof Property) {
077            type = Object.class;
078            if (base instanceof DocumentPropertyContext) {
079                DocumentPropertyContext ctx = (DocumentPropertyContext) base;
080                try {
081                    Property docProperty = getDocumentProperty(ctx, property);
082                    if (docProperty.isContainer()) {
083                        Property subProperty = getDocumentProperty(docProperty, property);
084                        if (subProperty.isList()) {
085                            type = List.class;
086                        }
087                    } else if (docProperty instanceof ArrayProperty) {
088                        type = List.class;
089                    }
090                } catch (PropertyException pe) {
091                    // avoid errors, return Object
092                    log.warn(pe.toString());
093                }
094            } else if (base instanceof Property) {
095                try {
096                    Property docProperty = (Property) base;
097                    Property subProperty = getDocumentProperty(docProperty, property);
098                    if (subProperty.isList()) {
099                        type = List.class;
100                    }
101                } catch (PropertyException pe) {
102                    try {
103                        // try property getters to resolve
104                        // doc.schema.field.type for instance
105                        type = super.getType(context, base, property);
106                    } catch (PropertyNotFoundException e) {
107                        // avoid errors, log original error and return Object
108                        log.warn(pe.toString());
109                    }
110                }
111            }
112            context.setPropertyResolved(true);
113        } else if (base instanceof Blob) {
114            type = super.getType(context, base, getBlobMapping(property));
115        }
116        return type;
117    }
118
119    @Override
120    public Object getValue(ELContext context, Object base, Object property) {
121        Object value = null;
122        if (base instanceof DocumentModel) {
123            try {
124                // try document getters first to resolve doc.id for instance
125                value = super.getValue(context, base, property);
126            } catch (PropertyNotFoundException e) {
127                value = new DocumentPropertyContext((DocumentModel) base, (String) property);
128                context.setPropertyResolved(true);
129            }
130        } else if (base instanceof DocumentPropertyContext) {
131            try {
132                DocumentPropertyContext ctx = (DocumentPropertyContext) base;
133                Property docProperty = getDocumentProperty(ctx, property);
134                value = getDocumentPropertyValue(docProperty);
135            } catch (PropertyException pe) {
136                // avoid errors, return null
137                log.warn(pe.toString());
138            }
139            context.setPropertyResolved(true);
140        } else if (base instanceof Property) {
141            try {
142                Property docProperty = (Property) base;
143                Property subProperty = getDocumentProperty(docProperty, property);
144                value = getDocumentPropertyValue(subProperty);
145            } catch (PropertyException pe) {
146                try {
147                    // try property getters to resolve doc.schema.field.type
148                    // for instance
149                    value = super.getValue(context, base, property);
150                } catch (PropertyNotFoundException e) {
151                    // avoid errors, log original error and return null
152                    log.warn(pe.toString());
153                }
154            }
155            context.setPropertyResolved(true);
156        } else if (base instanceof Blob) {
157            value = super.getValue(context, base, getBlobMapping(property));
158         }
159
160        return value;
161    }
162
163    private static String getDocumentPropertyName(DocumentPropertyContext ctx, Object propertyValue) {
164        return ctx.schema + ":" + propertyValue;
165    }
166
167    private static Property getDocumentProperty(DocumentPropertyContext ctx, Object propertyValue)
168            throws PropertyException {
169        return ctx.doc.getProperty(getDocumentPropertyName(ctx, propertyValue));
170    }
171
172    @SuppressWarnings("boxing")
173    private static Property getDocumentProperty(Property docProperty, Object propertyValue) throws PropertyException {
174        Property subProperty = null;
175        if ((docProperty instanceof ArrayProperty || docProperty instanceof ListProperty)
176                && propertyValue instanceof Long) {
177            subProperty = docProperty.get(((Long) propertyValue).intValue());
178        } else if ((docProperty instanceof ArrayProperty || docProperty instanceof ListProperty)
179                && propertyValue instanceof Integer) {
180            Integer idx = (Integer) propertyValue;
181            if (idx < docProperty.size()) {
182                subProperty = docProperty.get((Integer) propertyValue);
183            }
184        } else if (docProperty instanceof ComplexProperty && propertyValue instanceof String) {
185            subProperty = docProperty.get((String) propertyValue);
186        }
187        if (subProperty == null) {
188            throw new PropertyException(String.format("Could not resolve subproperty '%s' under '%s'", propertyValue,
189                    docProperty.getXPath()));
190        }
191        return subProperty;
192    }
193
194    private static Object getDocumentPropertyValue(Property docProperty) throws PropertyException {
195        if (docProperty == null) {
196            throw new PropertyException("Null property");
197        }
198        Object value = docProperty;
199        if (!docProperty.isContainer()) {
200            // return the value
201            value = docProperty.getValue();
202            value = FieldAdapterManager.getValueForDisplay(value);
203        }
204        return value;
205    }
206
207    /**
208     * Handle property mappings for blobs. The Blob use case is handled here too instead of a dedicated EL resolver to
209     * avoid multiplying resolvers in the chain.
210     */
211    private static Object getBlobMapping(Object property) throws PropertyException {
212        Object prop = property;
213        if ("name".equals(property)) {
214            prop = "filename";
215        } else if ("mime-type".equals(property)) {
216            prop = "mimeType";
217        }
218        return prop;
219    }
220
221    @Override
222    public boolean isReadOnly(ELContext context, Object base, Object property) {
223        boolean readOnly = false;
224        try {
225            readOnly = super.isReadOnly(context, base, property);
226        } catch (PropertyNotFoundException e) {
227            if (base instanceof DocumentModel || base instanceof DocumentPropertyContext) {
228                // readOnly is false
229                context.setPropertyResolved(true);
230            } else if (base instanceof Property) {
231                readOnly = ((Property) base).isReadOnly();
232                context.setPropertyResolved(true);
233            }
234        }
235        return readOnly;
236    }
237
238    @Override
239    @SuppressWarnings("unchecked")
240    public void setValue(ELContext context, Object base, Object property, Object value) {
241        if (base instanceof DocumentModel) {
242            try {
243                super.setValue(context, base, property, value);
244            } catch (PropertyNotFoundException e) {
245                // nothing else to set on doc model
246            }
247        } else if (base instanceof DocumentPropertyContext) {
248            DocumentPropertyContext ctx = (DocumentPropertyContext) base;
249            value = FieldAdapterManager.getValueForStorage(value);
250            try {
251                if ("files".equals(ctx.getSchema())) {
252                    // Remove possible null values if a file was deleted
253                    List<Map<String, Serializable>> files = (List<Map<String, Serializable>>) value;
254                    List<Map<String, Serializable>> filteredFiles = files.stream()
255                                                                         .filter(file -> file.get("file") != null)
256                                                                         .collect(Collectors.toList());
257                    ctx.doc.setPropertyValue(getDocumentPropertyName(ctx, property), (Serializable) filteredFiles);
258                } else {
259                    ctx.doc.setPropertyValue(getDocumentPropertyName(ctx, property), (Serializable) value);
260                }
261            } catch (PropertyException e) {
262                // avoid errors here too
263                log.warn(e.toString());
264            }
265            context.setPropertyResolved(true);
266        } else if (base instanceof Property) {
267            try {
268                Property docProperty = (Property) base;
269                // Remove possible null values if a file was deleted
270                if ("files".equals(docProperty.getSchema().getName()) && value == null) {
271                    docProperty.remove();
272                } else {
273                    Property subProperty = getDocumentProperty(docProperty, property);
274                    value = FieldAdapterManager.getValueForStorage(value);
275                    subProperty.setValue(value);
276                }
277            } catch (PropertyException pe) {
278                try {
279                    // try property setters to resolve doc.schema.field.type
280                    // for instance
281                    super.setValue(context, base, property, value);
282                } catch (PropertyNotFoundException e) {
283                    // log original error and avoid errors here too
284                    log.warn(pe.toString());
285                }
286            }
287            context.setPropertyResolved(true);
288        } else if (base instanceof Blob) {
289            super.setValue(context, base, getBlobMapping(property), value);
290        }
291    }
292
293}