001/*
002 * (C) Copyright 2014 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 *     vpasquier <vpasquier@nuxeo.com>
016 */
017package org.nuxeo.box.api.service;
018
019import com.fasterxml.jackson.core.JsonProcessingException;
020import com.fasterxml.jackson.databind.ObjectMapper;
021import com.google.common.collect.BiMap;
022import com.google.common.collect.HashBiMap;
023import org.nuxeo.box.api.BoxConstants;
024import org.nuxeo.box.api.adapter.BoxAdapter;
025import org.nuxeo.box.api.folder.adapter.BoxFolderAdapter;
026import org.nuxeo.box.api.marshalling.dao.BoxCollaboration;
027import org.nuxeo.box.api.marshalling.dao.BoxCollaborationRole;
028import org.nuxeo.box.api.marshalling.dao.BoxCollection;
029import org.nuxeo.box.api.marshalling.dao.BoxComment;
030import org.nuxeo.box.api.marshalling.dao.BoxFile;
031import org.nuxeo.box.api.marshalling.dao.BoxFolder;
032import org.nuxeo.box.api.marshalling.dao.BoxGroup;
033import org.nuxeo.box.api.marshalling.dao.BoxItem;
034import org.nuxeo.box.api.marshalling.dao.BoxObject;
035import org.nuxeo.box.api.marshalling.dao.BoxTypedObject;
036import org.nuxeo.box.api.marshalling.dao.BoxUser;
037import org.nuxeo.box.api.marshalling.exceptions.BoxJSONException;
038import org.nuxeo.box.api.marshalling.exceptions.BoxRestException;
039import org.nuxeo.box.api.marshalling.exceptions.NXBoxJsonException;
040import org.nuxeo.box.api.marshalling.jsonparsing.BoxJSONParser;
041import org.nuxeo.box.api.marshalling.jsonparsing.BoxResourceHub;
042import org.apache.commons.lang.StringUtils;
043import org.nuxeo.ecm.core.api.Blob;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentModelList;
047import org.nuxeo.ecm.core.api.NuxeoGroup;
048import org.nuxeo.ecm.core.api.NuxeoPrincipal;
049import org.nuxeo.ecm.core.api.security.ACE;
050import org.nuxeo.ecm.core.api.security.SecurityConstants;
051import org.nuxeo.ecm.platform.usermanager.UserManager;
052import org.nuxeo.runtime.api.Framework;
053
054import javax.ws.rs.core.Response;
055import java.util.ArrayList;
056import java.util.Collections;
057import java.util.HashMap;
058import java.util.List;
059import java.util.Map;
060
061/**
062 * Box Service Utils
063 *
064 * @since 5.9.3
065 */
066public class BoxServiceImpl implements BoxService {
067
068    /**
069     * The mapping between Nuxeo ACLs and Box Collaboration
070     */
071    protected final BiMap<String, String> nxBoxRole;
072
073    @Override
074    public BiMap<String, String> getNxBoxRole() {
075        return nxBoxRole;
076    }
077
078    public BoxServiceImpl() {
079        nxBoxRole = HashBiMap.create();
080        nxBoxRole.put(SecurityConstants.EVERYTHING, BoxCollaborationRole.EDITOR);
081        nxBoxRole.put(SecurityConstants.READ, BoxCollaborationRole.VIEWER);
082        nxBoxRole.put(SecurityConstants.WRITE, BoxCollaborationRole.VIEWER_UPLOADER);
083    }
084
085    @Override
086    public BoxCollection searchBox(String term, CoreSession session, String limit, String offset)
087            {
088        final Map<String, Object> collectionProperties = new HashMap<>();
089        StringBuilder query = new StringBuilder();
090        query.append("SELECT * FROM " + "Document where ecm:fulltext = '" + term + "'");
091        DocumentModelList documentModels = session.query(query.toString(), null, Long.parseLong(limit),
092                Long.parseLong(offset), false);
093        // Adapt all documents to box document listing to get all properties
094        List<BoxTypedObject> boxDocuments = new ArrayList<>();
095        for (DocumentModel doc : documentModels) {
096            BoxAdapter boxAdapter = doc.getAdapter(BoxAdapter.class);
097            boxDocuments.add(boxAdapter.getBoxItem());
098        }
099        collectionProperties.put(BoxCollection.FIELD_ENTRIES, boxDocuments);
100        collectionProperties.put(BoxCollection.FIELD_TOTAL_COUNT, documentModels.size());
101        return new BoxCollection(Collections.unmodifiableMap(collectionProperties));
102    }
103
104    @Override
105    public List<BoxTypedObject> getBoxDocumentCollection(DocumentModelList documentModels, String fields)
106            {
107
108        final List<BoxTypedObject> boxObject = new ArrayList<>();
109        for (DocumentModel documentModel : documentModels) {
110            final Map<String, Object> documentProperties = new HashMap<>();
111            documentProperties.put(BoxTypedObject.FIELD_ID, getBoxId(documentModel));
112            documentProperties.put(BoxItem.FIELD_SEQUENCE_ID, getBoxSequenceId(documentModel));
113            documentProperties.put(BoxItem.FIELD_ETAG, getBoxEtag(documentModel));
114            documentProperties.put(BoxItem.FIELD_NAME, getBoxName(documentModel));
115            // NX MD5 -> Box SHA1
116            if (documentModel.hasSchema("file")) {
117                Blob blob = (Blob) documentModel.getPropertyValue("file:content");
118                if (blob != null) {
119                    documentProperties.put(BoxFile.FIELD_SHA1, blob.getDigest());
120                }
121            }
122            // This different instantiation is related to the param type
123            // which is automatically added in json payload by Box marshaller
124            // following the box object type
125            BoxTypedObject boxChild;
126            boxChild = documentModel.isFolder() ? new BoxFolder() : new BoxFile();
127            // Depending of fields filter provided in the REST call:
128            // Properties setup (* -> all)
129            if (!"*".equals(fields) && fields != null) {
130                for (String field : fields.split(",")) {
131                    boxChild.put(field, documentProperties.get(field));
132                }
133            } else {
134                boxChild.putAll(documentProperties);
135            }
136            boxObject.add(boxChild);
137        }
138        return boxObject;
139    }
140
141    /**
142     * @param boxFolderAdapter the related box folder
143     * @param ace the specific ACE for this collaboration
144     * @return a box collaboration
145     */
146    @Override
147    public BoxCollaboration getBoxCollaboration(BoxFolderAdapter boxFolderAdapter, ACE ace, String collaborationId)
148            {
149        Map<String, Object> boxCollabProperties = new HashMap<>();
150        // Nuxeo acl doesn't provide id yet
151        boxCollabProperties.put(BoxCollaboration.FIELD_ID,
152                computeCollaborationId(boxFolderAdapter.getBoxItem().getId(), collaborationId));
153        // Nuxeo acl doesn't provide created date yet
154        boxCollabProperties.put(BoxCollaboration.FIELD_CREATED_AT, null);
155        // Nuxeo acl doesn't provide modified date yet
156        boxCollabProperties.put(BoxCollaboration.FIELD_MODIFIED_AT, null);
157
158        // Creator
159        final UserManager userManager = Framework.getLocalService(UserManager.class);
160        boxCollabProperties.put(BoxCollaboration.FIELD_CREATED_BY, boxFolderAdapter.getBoxItem().getCreatedBy());
161
162        // Nuxeo doesn't provide expiration date yet
163        boxCollabProperties.put(BoxCollaboration.FIELD_EXPIRES_AT, null);
164        // Nuxeo doesn't provide status on ACL setup (accepted...)
165        boxCollabProperties.put(BoxCollaboration.FIELD_STATUS, "active");
166        // Nuxeo doesn't provide acknowledge date on status (see
167        // just above)
168        boxCollabProperties.put(BoxCollaboration.FIELD_ACKNOWLEGED_AT, null);
169
170        // Document itself -> a mandatory folder
171        boxCollabProperties.put(BoxCollaboration.FIELD_FOLDER, boxFolderAdapter.getMiniItem());
172
173        // User or Group whom can access to the document
174        NuxeoPrincipal user = userManager.getPrincipal(ace.getUsername());
175        NuxeoGroup group = userManager.getGroup(ace.getUsername());
176        boxCollabProperties.put(BoxCollaboration.FIELD_ACCESSIBLE_BY, user != null ? fillUser(user) : fillGroup(group));
177
178        // Box Role
179        boxCollabProperties.put(BoxCollaboration.FIELD_ROLE, nxBoxRole.get(ace.getPermission()));
180        return new BoxCollaboration(boxCollabProperties);
181    }
182
183    /**
184     * Marshalling the box object to JSON
185     */
186    @Override
187    public String toJSONString(BoxObject boxObject) throws BoxJSONException {
188        BoxJSONParser boxJSONParser = new BoxJSONParser(new BoxResourceHub());
189        try {
190            return boxObject.toJSONString(boxJSONParser);
191        } catch (BoxJSONException e) {
192            throw new BoxRestException("Box Parser Exception", e, Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
193        }
194    }
195
196    /**
197     * Helpers to get Ids for sequence, etag and id itself. In case of root, sequence and etag are null and id = 0
198     * according to the box documentation.
199     */
200
201    @Override
202    public String getBoxId(DocumentModel doc) {
203        if (doc != null) {
204            return doc.getName() != null ? doc.getId() : "0";
205        }
206        return null;
207    }
208
209    @Override
210    public String getBoxSequenceId(DocumentModel doc) {
211        if (doc != null) {
212            return doc.getName() != null ? doc.getId() : null;
213        }
214        return null;
215    }
216
217    @Override
218    public String getBoxEtag(DocumentModel doc) {
219        if (doc != null) {
220            return doc.getName() != null ? doc.getId() + "_" + doc.getVersionLabel() : null;
221        }
222        return null;
223    }
224
225    @Override
226    public String getBoxName(DocumentModel doc) {
227        if (doc != null) {
228            return doc.getName() != null ? doc.getName() : "/";
229        }
230        return null;
231    }
232
233    /**
234     * Return a box user from a Nuxeo user metamodel
235     */
236    @Override
237    public BoxUser fillUser(NuxeoPrincipal creator) {
238        final Map<String, Object> mapUser = new HashMap<>();
239        mapUser.put(BoxItem.FIELD_ID, creator != null ? creator.getPrincipalId() : "system");
240        mapUser.put(BoxItem.FIELD_NAME, creator != null ? creator.getFirstName() + " " + creator.getLastName()
241                : "system");
242        mapUser.put(BoxUser.FIELD_LOGIN, creator != null ? creator.getName() : "system");
243        return new BoxUser(Collections.unmodifiableMap(mapUser));
244    }
245
246    /**
247     * Return a box group from a Nuxeo user metamodel
248     */
249    @Override
250    public BoxGroup fillGroup(NuxeoGroup group) {
251        final Map<String, Object> mapGroup = new HashMap<>();
252        mapGroup.put(BoxItem.FIELD_ID, group != null ? group.getName() : "system");
253        mapGroup.put(BoxItem.FIELD_NAME, group != null ? group.getLabel() : "system");
254        mapGroup.put(BoxUser.FIELD_LOGIN, group != null ? group.getName() : "system");
255        return new BoxGroup(Collections.unmodifiableMap(mapGroup));
256    }
257
258    @Override
259    public BoxFolder getBoxFolder(String jsonBoxFolder) throws BoxJSONException {
260        return new BoxJSONParser(new BoxResourceHub()).parseIntoBoxObject(jsonBoxFolder, BoxFolder.class);
261    }
262
263    @Override
264    public BoxFile getBoxFile(String jsonBoxFile) throws BoxJSONException {
265        return new BoxJSONParser(new BoxResourceHub()).parseIntoBoxObject(jsonBoxFile, BoxFile.class);
266    }
267
268    @Override
269    public BoxComment getBoxComment(String jsonBoxComment) throws BoxJSONException {
270        return new BoxJSONParser(new BoxResourceHub()).parseIntoBoxObject(jsonBoxComment, BoxComment.class);
271    }
272
273    @Override
274    public BoxCollaboration getBoxCollaboration(String jsonBoxCollaboration) throws BoxJSONException {
275        return new BoxJSONParser(new BoxResourceHub()).parseIntoBoxObject(jsonBoxCollaboration, BoxCollaboration.class);
276    }
277
278    @Override
279    public String getJSONFromBox(BoxTypedObject boxTypedObject) throws BoxJSONException {
280        return boxTypedObject.toJSONString(new BoxJSONParser(new BoxResourceHub()));
281    }
282
283    /**
284     * @return a Box Exception Response in JSON
285     */
286    @Override
287    public String getJSONBoxException(Exception e, int status) {
288        NXBoxJsonException boxException = new NXBoxJsonException();
289        // Message
290        boxException.setCode(e.getMessage());
291        // Detailed Message
292        boxException.setMessage(e.getCause() != null ? e.getCause().getMessage() : null);
293        boxException.setStatus(status);
294        ObjectMapper mapper = new ObjectMapper();
295        String jsonExceptionResponse = StringUtils.EMPTY;
296        try {
297            jsonExceptionResponse = mapper.writeValueAsString(boxException);
298        } catch (JsonProcessingException e1) {
299            throw new BoxRestException("error when marshalling server " + "exception:", e1,
300                    Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
301        }
302        return jsonExceptionResponse;
303    }
304
305    /**
306     * @return the array containing Folder Id and Collab Id
307     */
308    @Override
309    public String[] getCollaborationArrayIds(String collaborationId) {
310        String[] collaborationIds = collaborationId.split(BoxConstants.BOX_COLLAB_DELIM);
311        if (collaborationIds.length == 0) {
312            return new String[2];
313        }
314        return collaborationIds;
315    }
316
317    public String computeCollaborationId(String folderId, String collaborationId) {
318        return folderId.concat(BoxConstants.BOX_COLLAB_DELIM).concat(collaborationId);
319    }
320}