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