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