001/*
002 * (C) Copyright 2013 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 *     dmetzler
016 */
017package org.nuxeo.ecm.automation.jaxrs.io.documents;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.io.Serializable;
022import java.lang.annotation.Annotation;
023import java.lang.reflect.Type;
024
025import javax.servlet.http.HttpServletRequest;
026import javax.ws.rs.Consumes;
027import javax.ws.rs.WebApplicationException;
028import javax.ws.rs.core.Context;
029import javax.ws.rs.core.MediaType;
030import javax.ws.rs.core.MultivaluedMap;
031import javax.ws.rs.core.Response;
032import javax.ws.rs.ext.MessageBodyReader;
033import javax.ws.rs.ext.Provider;
034
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.lang.StringUtils;
037import org.apache.commons.logging.Log;
038import org.apache.commons.logging.LogFactory;
039import org.codehaus.jackson.JsonFactory;
040import org.codehaus.jackson.JsonNode;
041import org.codehaus.jackson.JsonParser;
042import org.codehaus.jackson.JsonToken;
043import org.nuxeo.ecm.automation.core.util.DocumentHelper;
044import org.nuxeo.ecm.automation.core.util.Properties;
045import org.nuxeo.ecm.core.api.Blob;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.IdRef;
049import org.nuxeo.ecm.core.api.impl.DataModelImpl;
050import org.nuxeo.ecm.core.api.impl.SimpleDocumentModel;
051import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
052import org.nuxeo.ecm.core.api.model.impl.primitives.BlobProperty;
053import org.nuxeo.ecm.webengine.WebException;
054import org.nuxeo.ecm.webengine.jaxrs.session.SessionFactory;
055
056/**
057 * JAX-RS reader for a DocumentModel. If an id is given, it tries to reattach the document to the session. If not, it
058 * creates a ready to create DocumentModel filled with the properties found.
059 *
060 * @since 5.7.2
061 */
062@Provider
063@Consumes({ "application/json+nxentity", "application/json" })
064public class JSONDocumentModelReader implements MessageBodyReader<DocumentModel> {
065
066    // private static final String REQUEST_BATCH_ID = "batchId";
067
068    protected static final Log log = LogFactory.getLog(JSONDocumentModelReader.class);
069
070    @Context
071    HttpServletRequest request;
072
073    @Context
074    JsonFactory factory;
075
076    @Override
077    public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
078        return DocumentModel.class.isAssignableFrom(type);
079    }
080
081    @Override
082    public DocumentModel readFrom(Class<DocumentModel> type, Type genericType, Annotation[] annotations,
083            MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
084            throws IOException, WebApplicationException {
085        String content = IOUtils.toString(entityStream);
086        if (content.isEmpty()) {
087            if (content.isEmpty()) {
088                throw new WebException("No content in request body", Response.Status.BAD_REQUEST.getStatusCode());
089            }
090
091        }
092
093        try {
094            return readRequest(content, httpHeaders);
095        } catch (IOException e) {
096            throw WebException.wrap(e);
097        }
098    }
099
100    private DocumentModel readRequest(String content, MultivaluedMap<String, String> httpHeaders) throws IOException {
101        return readRequest(content, httpHeaders, request);
102    }
103
104    protected DocumentModel readRequest(String content, MultivaluedMap<String, String> httpHeaders,
105            HttpServletRequest request) throws IOException {
106        JsonParser jp = factory.createJsonParser(content);
107        return readJson(jp, httpHeaders, request);
108    }
109
110    public static DocumentModel readJson(JsonParser jp, MultivaluedMap<String, String> httpHeaders,
111            HttpServletRequest request) throws IOException {
112        JsonToken tok = jp.nextToken();
113
114        // skip {
115        if (jp.getCurrentToken() == JsonToken.START_OBJECT) {
116            tok = jp.nextToken();
117        }
118        SimpleDocumentModel simpleDoc = new SimpleDocumentModel();
119        String type = null;
120        String name = null;
121        String uid = null;
122        while (tok != null && tok != JsonToken.END_OBJECT) {
123            String key = jp.getCurrentName();
124            jp.nextToken();
125            if ("properties".equals(key)) {
126                DocumentHelper.setJSONProperties(null, simpleDoc, readProperties(jp));
127            } else if ("name".equals(key)) {
128                name = jp.readValueAs(String.class);
129            } else if ("type".equals(key)) {
130                type = jp.readValueAs(String.class);
131            } else if ("uid".equals(key)) {
132                uid = jp.readValueAs(String.class);
133            } else if ("entity-type".equals(key)) {
134                String entityType = jp.readValueAs(String.class);
135                if (!"document".equals(entityType)) {
136                    throw new WebApplicationException(Response.Status.BAD_REQUEST);
137                }
138            } else {
139                log.debug("Unknown key: " + key);
140                jp.skipChildren();
141            }
142
143            tok = jp.nextToken();
144        }
145
146        if (tok == null) {
147            throw new IllegalArgumentException("Unexpected end of stream.");
148        }
149
150        if (StringUtils.isNotBlank(type)) {
151            simpleDoc.setType(type);
152        }
153
154        if (StringUtils.isNotBlank(name)) {
155            simpleDoc.setPathInfo(null, name);
156        }
157
158        // If a uid is specified, we try to get the doc from
159        // the core session
160        if (uid != null) {
161            CoreSession session = SessionFactory.getSession(request);
162            DocumentModel doc = session.getDocument(new IdRef(uid));
163            applyPropertyValues(simpleDoc, doc);
164            return doc;
165        } else {
166            return simpleDoc;
167        }
168
169    }
170
171    static Properties readProperties(JsonParser jp) throws IOException {
172        JsonNode node = jp.readValueAsTree();
173        return new Properties(node);
174
175    }
176
177    /**
178     * Decodes a Serializable to make it a blob.
179     *
180     * @since 5.9.1
181     */
182    private static Serializable decodeBlob(Serializable data) {
183        if (data instanceof Blob) {
184            return data;
185        } else {
186            return null;
187        }
188    }
189
190    public static void applyPropertyValues(DocumentModel src, DocumentModel dst) {
191        for (String schema : src.getSchemas()) {
192            DataModelImpl dataModel = (DataModelImpl) dst.getDataModel(schema);
193            DataModelImpl fromDataModel = (DataModelImpl) src.getDataModel(schema);
194
195            for (String field : fromDataModel.getDirtyFields()) {
196                Serializable data = (Serializable) fromDataModel.getData(field);
197                try {
198                    if (!(dataModel.getDocumentPart().get(field) instanceof BlobProperty)) {
199                        dataModel.setData(field, data);
200                    } else {
201                        dataModel.setData(field, decodeBlob(data));
202                    }
203                    // }
204                } catch (PropertyNotFoundException e) {
205                    log.warn(String.format("Trying to deserialize unexistent field : {%s}", field));
206                }
207            }
208        }
209    }
210
211}