001/*
002 * (C) Copyright 2016 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 *     bstefanescu
018 */
019package org.nuxeo.automation.scripting.internals;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Set;
029import java.util.stream.Collectors;
030import java.util.stream.Stream;
031
032import org.nuxeo.ecm.automation.core.util.BlobList;
033import org.nuxeo.ecm.automation.core.util.DataModelProperties;
034import org.nuxeo.ecm.automation.core.util.Properties;
035import org.nuxeo.ecm.core.api.Blob;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.DocumentRef;
040import org.nuxeo.ecm.core.api.PathRef;
041import org.nuxeo.ecm.core.api.PropertyException;
042import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
043import org.nuxeo.ecm.core.api.model.Property;
044import org.nuxeo.ecm.core.schema.DocumentType;
045
046import jdk.nashorn.api.scripting.ScriptObjectMirror;
047
048/**
049 * Wrap a {@link DocumentModel} to expose in a pretty way more information to automation scripts.
050 *
051 * @since 8.4
052 */
053public class DocumentScriptingWrapper extends HashMap<String, Object> {
054
055    private static final long serialVersionUID = 1L;
056
057    protected final AutomationMapper mapper;
058
059    protected final DocumentModel doc;
060
061    public static Object wrap(Object object, AutomationMapper mapper) {
062        if (object == null) {
063            return null;
064        }
065        if (object instanceof DocumentModel) {
066            return new DocumentScriptingWrapper(mapper, (DocumentModel) object);
067        } else if (object instanceof DocumentModelList) {
068            List<DocumentScriptingWrapper> docs = new ArrayList<>();
069            for (DocumentModel doc : (DocumentModelList) object) {
070                docs.add(new DocumentScriptingWrapper(mapper, doc));
071            }
072            return docs;
073        } else if (object instanceof Map<?, ?>) {
074            @SuppressWarnings("unchecked")
075            Map<String, Object> m = (Map<String, Object>) object;
076            return wrap(m, mapper);
077        }
078        return object;
079    }
080
081    public static Map<String, Object> wrap(Map<String, Object> source, AutomationMapper mapper) {
082        return source.entrySet().stream()
083                .collect(Collectors.toMap(Map.Entry::getKey, e -> wrap(e.getValue(), mapper)));
084    }
085
086    public static Object unwrap(Object object) {
087        // First unwrap object if it's a nashorn object
088        Object result = object;
089        if (result instanceof ScriptObjectMirror) {
090            result = ScriptObjectMirrors.unwrap((ScriptObjectMirror) result);
091        }
092        // Second unwrap object
093        if (result instanceof DocumentScriptingWrapper) {
094            result = ((DocumentScriptingWrapper) result).getDoc();
095        } else if (result instanceof List<?>) {
096            List<?> l = (List<?>) result;
097            // Several possible cases here:
098            // - l is of type DocumentModelList or BlobList -> already in right type
099            // - l is a list of DocumentScriptingWrapper -> elements need to be unwrapped into a DocumentModelList
100            // - l is a list of DocumentWrapper -> l needs to be converted to DocumentModelList
101            // - l is a list of Blob -> l needs to be converted to BlobList
102            // - l is a list -> do nothing
103            if (l.size() > 0 && !(result instanceof DocumentModelList || result instanceof BlobList)) {
104                Object first = l.get(0);
105                if (first instanceof DocumentModel) {
106                    result = l.stream().map(DocumentModel.class::cast)
107                            .collect(Collectors.toCollection(DocumentModelListImpl::new));
108                } else if (first instanceof Blob) {
109                    result = l.stream().map(Blob.class::cast).collect(Collectors.toCollection(BlobList::new));
110                } else if (first instanceof DocumentScriptingWrapper) {
111                    result = l.stream().map(DocumentScriptingWrapper.class::cast).map(DocumentScriptingWrapper::getDoc)
112                            .collect(Collectors.toCollection(DocumentModelListImpl::new));
113                }
114            }
115        } else if (result instanceof Map<?, ?>) {
116            @SuppressWarnings("unchecked")
117            final Map<String, Object> map = (Map<String, Object>) result;
118            result = computeProperties(map);
119        }
120        return result;
121    }
122
123    protected static Properties computeProperties(Map<?, ?> result) {
124        DataModelProperties props = new DataModelProperties();
125        for (Entry<?, ?> entry : result.entrySet()) {
126            props.getMap().put(entry.getKey().toString(), (Serializable) entry.getValue());
127        }
128        return props;
129    }
130
131    public static Map<String, Object> unwrap(Map<String, Object> source) {
132        return source.entrySet().stream().filter(e -> e.getValue() != null)
133                .collect(Collectors.toMap(Map.Entry::getKey, e -> unwrap(e.getValue())));
134    }
135
136    public DocumentScriptingWrapper(AutomationMapper mapper, DocumentModel doc) {
137        this.mapper = mapper;
138        this.doc = doc;
139    }
140
141    public DocumentModel getDoc() {
142        return doc;
143    }
144
145    public CoreSession getSession() {
146        return mapper.ctx.getCoreSession();
147    }
148
149    public DocumentScriptingWrapper getParent() {
150        DocumentModel parent = getSession().getParentDocument(doc.getRef());
151        return parent != null ? new DocumentScriptingWrapper(mapper, parent) : null;
152    }
153
154    public DocumentScriptingWrapper getParent(String type) {
155        DocumentModel parent = getSession().getParentDocument(doc.getRef());
156        while (parent != null && !type.equals(parent.getType())) {
157            parent = getSession().getParentDocument(parent.getRef());
158        }
159        if (parent == null) {
160            return null;
161        }
162        return new DocumentScriptingWrapper(mapper, parent);
163    }
164
165    public DocumentScriptingWrapper getWorkspace() {
166        return getParent("Workspace");
167    }
168
169    public DocumentScriptingWrapper getDomain() {
170        return getParent("Domain");
171    }
172
173    public String getTitle() {
174        return doc.getTitle();
175    }
176
177    public String getPath() {
178        return doc.getPathAsString();
179    }
180
181    public String resolvePath(String relative) {
182        return doc.getPath().append(relative).toString();
183    }
184
185    /**
186     * @return the document ref
187     */
188    public DocumentRef getRef() {
189        return doc.getRef();
190    }
191
192    public DocumentRef resolvePathAsRef(String relative) {
193        return new PathRef(doc.getPath().append(relative).toString());
194    }
195
196    public String getDescription() {
197        return (String) doc.getPropertyValue("dc:description");
198    }
199
200    public boolean hasFacet(String facet) {
201        return doc.hasFacet(facet);
202    }
203
204    public boolean hasSchema(String schema) {
205        return doc.hasSchema(schema);
206    }
207
208    public boolean addFacet(String facet) {
209        return doc.addFacet(facet);
210    }
211
212    public boolean removeFacet(String facet) {
213        return doc.removeFacet(facet);
214    }
215
216    public String getType() {
217        return doc.getType();
218    }
219
220    public DocumentType getDocumentType() {
221        return doc.getDocumentType();
222    }
223
224    public String getLifeCycle() {
225        return doc.getCurrentLifeCycleState();
226    }
227
228    public boolean isLocked() {
229        return doc.isLocked();
230    }
231
232    public boolean isFolder() {
233        return doc.isFolder();
234    }
235
236    public boolean isImmutable() {
237        return doc.isImmutable();
238    }
239
240    public boolean isProxy() {
241        return doc.isProxy();
242    }
243
244    public boolean isVersion() {
245        return doc.isVersion();
246    }
247
248    public boolean isDownloadable() {
249        return doc.isDownloadable();
250    }
251
252    public boolean isVersionable() {
253        return doc.isVersionable();
254    }
255
256    public String getId() {
257        return doc.getId();
258    }
259
260    public String getName() {
261        return doc.getName();
262    }
263
264    public String[] getSchemas() {
265        return doc.getSchemas();
266    }
267
268    public Set<String> getFacets() {
269        return doc.getFacets();
270    }
271
272    public Serializable getProperty(String key) {
273        return doc.getPropertyValue(key);
274    }
275
276    /**
277     * Alias for #getProperty.
278     */
279    public Serializable getPropertyValue(String key) {
280        return doc.getPropertyValue(key);
281    }
282
283    public void setProperty(String key, Serializable value) {
284        doc.setPropertyValue(key, value);
285    }
286
287    /**
288     * Alias for #setProperty.
289     */
290    public void setPropertyValue(String key, Serializable value) {
291        doc.setPropertyValue(key, value);
292    }
293
294    /**
295     * Used by nashorn for native javascript array/date.
296     */
297    public void setPropertyValue(String key, ScriptObjectMirror value) {
298        doc.setPropertyValue(key, (Serializable) ScriptObjectMirrors.unwrap(value));
299    }
300
301    public String getVersionLabel() {
302        return doc.getVersionLabel();
303    }
304
305    /** property map implementation */
306
307    @Override
308    public boolean containsKey(Object key) {
309        try {
310            doc.getProperty(key.toString());
311            return true;
312        } catch (PropertyException e) {
313            return false;
314        }
315    }
316
317    /**
318     * The behavior of this method was changed -&gt; it is checking if an xpath has a value attached.
319     */
320    @Override
321    public boolean containsValue(Object value) {
322        try {
323            return doc.getProperty(value.toString()).getValue() != null;
324        } catch (PropertyException e) {
325            return false;
326        }
327    }
328
329    @Override
330    public Serializable get(Object key) {
331        try {
332            return doc.getProperty(key.toString()).getValue();
333        } catch (PropertyException e) {
334            return null;
335        }
336    }
337
338    @Override
339    public boolean isEmpty() {
340        return false;
341    }
342
343    @Override
344    public int size() {
345        return Stream.of(doc.getParts()).collect(Collectors.summingInt(part -> part.size()));
346    }
347
348    @Override
349    public Set<String> keySet() {
350        return Collections.unmodifiableSet(Stream.of(doc.getSchemas())
351                .map(name -> doc.getProperties(name).keySet().stream()).flatMap(s -> s).collect(Collectors.toSet()));
352    }
353
354    @Override
355    public Collection<Object> values() {
356        return Collections.unmodifiableCollection(Stream.of(doc.getSchemas())
357                .map(name -> doc.getProperties(name).values().stream()).flatMap(s -> s).collect(Collectors.toSet()));
358    }
359
360    @Override
361    public Set<Entry<String, Object>> entrySet() {
362        return Collections.unmodifiableSet(Stream.of(doc.getSchemas())
363                .flatMap(name -> doc.getProperties(name).entrySet().stream()).collect(Collectors.toSet()));
364    }
365
366    /**
367     * As we need to handle {@link ScriptObjectMirror} for array type from nashorn.
368     */
369    @Override
370    public Object put(String key, Object value) {
371        if (value instanceof ScriptObjectMirror) {
372            return put(key, (Serializable) ScriptObjectMirrors.unwrap((ScriptObjectMirror) value));
373        }
374        return put(key, (Serializable) value);
375    }
376
377    public Serializable put(String key, Serializable value) {
378        Property p = doc.getProperty(key);
379        Serializable v = p.getValue();
380        p.setValue(value);
381        return v;
382    }
383
384    @Override
385    public void putAll(Map<? extends String, ?> m) {
386        throw new UnsupportedOperationException("Read Only Map.");
387    }
388
389    @Override
390    public Serializable remove(Object key) {
391        throw new UnsupportedOperationException("Read Only Map.");
392    }
393
394    @Override
395    public void clear() {
396        throw new UnsupportedOperationException("Read Only Map.");
397    }
398
399    @Override
400    public String toString() {
401        return doc.toString();
402    }
403
404}