001/*
002 * ModeShape (http://www.modeshape.org)
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 */
016package org.modeshape.cmis;
017
018import java.math.BigInteger;
019import java.util.ArrayList;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024import javax.jcr.Credentials;
025import javax.jcr.Session;
026import javax.jcr.SimpleCredentials;
027import javax.servlet.http.HttpServletRequest;
028import org.apache.chemistry.opencmis.commons.data.Acl;
029import org.apache.chemistry.opencmis.commons.data.AllowableActions;
030import org.apache.chemistry.opencmis.commons.data.ContentStream;
031import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
032import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
033import org.apache.chemistry.opencmis.commons.data.ObjectData;
034import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
035import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
036import org.apache.chemistry.opencmis.commons.data.ObjectList;
037import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
038import org.apache.chemistry.opencmis.commons.data.Properties;
039import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
040import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
041import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
042import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
043import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
044import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
045import org.apache.chemistry.opencmis.commons.enums.VersioningState;
046import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
047import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
048import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
049import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
050import org.apache.chemistry.opencmis.commons.server.CallContext;
051import org.apache.chemistry.opencmis.commons.spi.Holder;
052import org.apache.chemistry.opencmis.jcr.JcrRepository;
053import org.apache.chemistry.opencmis.server.support.wrapper.CallContextAwareCmisService;
054import org.modeshape.common.util.CheckArg;
055import org.modeshape.jcr.api.ServletCredentials;
056import org.modeshape.web.jcr.WebLogger;
057
058/**
059 * JCR service implementation.
060 *
061 * @author kulikov
062 */
063public class JcrService extends AbstractCmisService implements CallContextAwareCmisService {
064
065    private final static org.modeshape.jcr.api.Logger LOGGER = WebLogger.getLogger(JcrService.class);
066    private final Map<String, JcrRepository> jcrRepositories;
067    private final Map<String, Session> sessions = new HashMap<String, Session>();
068    private CallContext context;
069
070    public JcrService(Map<String, JcrRepository> jcrRepositories) {
071        this.jcrRepositories = jcrRepositories;
072    }
073
074    @Override
075    public void close() {
076        for (Session session : sessions.values()) {
077            session.logout();
078        }
079
080        super.close();
081    }
082
083    @Override
084    public void setCallContext(CallContext context) {
085        this.context = context;
086    }
087
088    @Override
089    public CallContext getCallContext() {
090        return this.context;
091    }
092
093    //------------------------------------------< repository service >---
094    @Override
095    public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) {
096        LOGGER.debug("-- getting repository info");
097        RepositoryInfo info = jcrRepository(repositoryId).getRepositoryInfo(login(repositoryId));
098        return new RepositoryInfoLocal(repositoryId, info);
099    }
100
101    @Override
102    public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {
103        ArrayList<RepositoryInfo> info = new ArrayList<>();
104        Set<String> IDs = jcrRepositories.keySet();
105        for (String Id : IDs) {
106            JcrRepository repo = jcrRepositories.get(Id);
107            List<RepositoryInfo> infos = repo.getRepositoryInfos(login(Id));
108            for (RepositoryInfo i : infos) {
109                info.add(new RepositoryInfoLocal(Id, i));
110            }
111        }
112        return info;
113    }
114
115    @Override
116    public TypeDefinitionList getTypeChildren(String repositoryId, String typeId, Boolean includePropertyDefinitions,
117            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
118
119        return jcrRepository(repositoryId).getTypeChildren(login(repositoryId), typeId, includePropertyDefinitions, maxItems, skipCount);
120    }
121
122    @Override
123    public TypeDefinition getTypeDefinition(String repositoryId, String typeId, ExtensionsData extension) {
124        return jcrRepository(repositoryId).getTypeDefinition(login(repositoryId), typeId);
125    }
126
127    @Override
128    public List<TypeDefinitionContainer> getTypeDescendants(String repositoryId, String typeId, BigInteger depth,
129            Boolean includePropertyDefinitions, ExtensionsData extension) {
130
131        return jcrRepository(repositoryId).getTypesDescendants(login(repositoryId), typeId, depth, includePropertyDefinitions);
132    }
133
134    //------------------------------------------< navigation service >---
135    @Override
136    public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
137            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
138            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
139
140        return jcrRepository(repositoryId).getChildren(login(repositoryId), folderId, filter, includeAllowableActions,
141                includePathSegment, maxItems, skipCount, this, context.isObjectInfoRequired());
142    }
143
144    @Override
145    public List<ObjectInFolderContainer> getDescendants(String repositoryId, String folderId, BigInteger depth,
146            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
147            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
148
149        return jcrRepository(repositoryId).getDescendants(login(repositoryId), folderId, depth, filter, includeAllowableActions,
150                includePathSegment, this, context.isObjectInfoRequired(), false);
151    }
152
153    @Override
154    public ObjectData getFolderParent(String repositoryId, String folderId, String filter, ExtensionsData extension) {
155        return jcrRepository(repositoryId).getFolderParent(login(repositoryId), folderId, filter, this, context.isObjectInfoRequired());
156    }
157
158    @Override
159    public List<ObjectInFolderContainer> getFolderTree(String repositoryId, String folderId, BigInteger depth,
160            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
161            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
162
163        return jcrRepository(repositoryId).getDescendants(login(repositoryId), folderId, depth, filter, includeAllowableActions,
164                includePathSegment, this, context.isObjectInfoRequired(), true);
165    }
166
167    @Override
168    public List<ObjectParentData> getObjectParents(String repositoryId, String objectId, String filter,
169            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
170            Boolean includeRelativePathSegment, ExtensionsData extension) {
171
172        return jcrRepository(repositoryId).getObjectParents(login(repositoryId), objectId, filter, includeAllowableActions,
173                includeRelativePathSegment, this, context.isObjectInfoRequired());
174    }
175
176    @Override
177    public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
178            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
179            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
180
181        return jcrRepository(repositoryId).getCheckedOutDocs(login(repositoryId), folderId, filter, orderBy, includeAllowableActions,
182                maxItems, skipCount);
183    }
184
185    //------------------------------------------< object service >---
186    @Override
187    public String createDocument(String repositoryId, Properties properties, String folderId,
188            ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
189            Acl removeAces, ExtensionsData extension) {
190
191        return jcrRepository(repositoryId).createDocument(login(repositoryId), properties, folderId, contentStream, versioningState);
192    }
193
194    @Override
195    public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties,
196            String folderId, VersioningState versioningState, List<String> policies, Acl addAces, Acl removeAces,
197            ExtensionsData extension) {
198
199        return jcrRepository(repositoryId).createDocumentFromSource(login(repositoryId), sourceId, properties, folderId, versioningState);
200    }
201
202    @Override
203    public void setContentStream(String repositoryId, Holder<String> objectId, Boolean overwriteFlag,
204            Holder<String> changeToken, ContentStream contentStream, ExtensionsData extension) {
205
206        jcrRepository(repositoryId).setContentStream(login(repositoryId), objectId, overwriteFlag, contentStream);
207    }
208
209    @Override
210    public void deleteContentStream(String repositoryId, Holder<String> objectId, Holder<String> changeToken,
211            ExtensionsData extension) {
212
213        jcrRepository(repositoryId).setContentStream(login(repositoryId), objectId, true, null);
214    }
215
216    @Override
217    public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
218            Acl addAces, Acl removeAces, ExtensionsData extension) {
219
220        return jcrRepository(repositoryId).createFolder(login(repositoryId), properties, folderId);
221    }
222
223    @Override
224    public void deleteObjectOrCancelCheckOut(String repositoryId, String objectId, Boolean allVersions,
225            ExtensionsData extension) {
226
227        jcrRepository(repositoryId).deleteObject(login(repositoryId), objectId, allVersions);
228    }
229
230    @Override
231    public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
232            UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
233
234        return jcrRepository(repositoryId).deleteTree(login(repositoryId), folderId);
235    }
236
237    @Override
238    public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
239        return jcrRepository(repositoryId).getAllowableActions(login(repositoryId), objectId);
240    }
241
242    @Override
243    public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
244            BigInteger length, ExtensionsData extension) {
245
246        return jcrRepository(repositoryId).getContentStream(login(repositoryId), objectId, offset, length);
247    }
248
249    @Override
250    public ObjectData getObject(String repositoryId, String objectId, String filter, Boolean includeAllowableActions,
251            IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
252            Boolean includeAcl, ExtensionsData extension) {
253
254        return jcrRepository(repositoryId).getObject(login(repositoryId), objectId, filter, includeAllowableActions, this,
255                context.isObjectInfoRequired());
256    }
257
258    @Override
259    public ObjectData getObjectByPath(String repositoryId, String path, String filter, Boolean includeAllowableActions,
260            IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
261            Boolean includeAcl, ExtensionsData extension) {
262
263        return jcrRepository(repositoryId).getObjectByPath(login(repositoryId), path, filter, includeAllowableActions, includeAcl,
264                this, context.isObjectInfoRequired());
265    }
266
267    @Override
268    public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
269        return jcrRepository(repositoryId).getProperties(login(repositoryId), objectId, filter, false, this,
270                context.isObjectInfoRequired());
271    }
272
273    @Override
274    public void moveObject(String repositoryId, Holder<String> objectId, String targetFolderId, String sourceFolderId,
275            ExtensionsData extension) {
276
277        jcrRepository(repositoryId).moveObject(login(repositoryId), objectId, targetFolderId, this, context.isObjectInfoRequired());
278    }
279
280    @Override
281    public void updateProperties(String repositoryId, Holder<String> objectId, Holder<String> changeToken,
282            Properties properties, ExtensionsData extension) {
283
284        jcrRepository(repositoryId).updateProperties(login(repositoryId), objectId, properties, this, context.isObjectInfoRequired());
285    }
286
287    //------------------------------------------< versioning service >---
288    @Override
289    public void checkOut(String repositoryId, Holder<String> objectId, ExtensionsData extension,
290            Holder<Boolean> contentCopied) {
291
292        jcrRepository(repositoryId).checkOut(login(repositoryId), objectId, contentCopied);
293    }
294
295    @Override
296    public void cancelCheckOut(String repositoryId, String objectId, ExtensionsData extension) {
297        jcrRepository(repositoryId).cancelCheckout(login(repositoryId), objectId);
298    }
299
300    @Override
301    public void checkIn(String repositoryId, Holder<String> objectId, Boolean major, Properties properties,
302            ContentStream contentStream, String checkinComment, List<String> policies, Acl addAces, Acl removeAces,
303            ExtensionsData extension) {
304
305        jcrRepository(repositoryId).checkIn(login(repositoryId), objectId, major, properties, contentStream, checkinComment);
306    }
307
308    @Override
309    public List<ObjectData> getAllVersions(String repositoryId, String objectId, String versionSeriesId, String filter,
310            Boolean includeAllowableActions, ExtensionsData extension) {
311
312        return jcrRepository(repositoryId).getAllVersions(login(repositoryId), versionSeriesId == null ? objectId : versionSeriesId,
313                filter, includeAllowableActions, this, context.isObjectInfoRequired());
314    }
315
316    @Override
317    public ObjectData getObjectOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
318            Boolean major, String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
319            String renditionFilter, Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
320
321        return jcrRepository(repositoryId).getObject(login(repositoryId), versionSeriesId == null ? objectId : versionSeriesId,
322                filter, includeAllowableActions, this, context.isObjectInfoRequired());
323    }
324
325    @Override
326    public Properties getPropertiesOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
327            Boolean major, String filter, ExtensionsData extension) {
328
329        ObjectData object = getObjectOfLatestVersion(repositoryId, objectId, versionSeriesId, major, filter, false,
330                null, null, false, false, extension);
331
332        return object.getProperties();
333    }
334
335    // --- discovery service ---
336    @Override
337    public ObjectList query(String repositoryId, String statement, Boolean searchAllVersions,
338            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
339            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
340
341        return jcrRepository(repositoryId).query(login(repositoryId), statement, searchAllVersions, includeAllowableActions,
342                maxItems, skipCount);
343    }
344
345    //------------------------------------------< protected >---
346    protected Session login(String repositoryId) {
347        LOGGER.debug("--- login: " + repositoryId);
348
349        if (context == null) {
350            throw new CmisRuntimeException("No user context!");
351        }
352
353        Session session = sessions.get(repositoryId);
354        if (session == null) {
355            HttpServletRequest request = (HttpServletRequest) context.get(CallContext.HTTP_SERVLET_REQUEST);
356            if (request != null) {
357                //try via http authentication
358                try {
359                    session = jcrRepository(repositoryId).login(new ServletCredentials(request), workspace(repositoryId));
360                    sessions.put(repositoryId, session);
361                    return session;
362                } catch (CmisPermissionDeniedException e) {
363                    LOGGER.debug(e, "Cannot authenticate using http authentication");
364                }
365            }
366            //http authentication didn't work, try std authentication
367            String userName = context.getUsername();
368            String password = context.getPassword();
369            Credentials credentials = (userName == null)
370                    ? null : new SimpleCredentials(userName, password == null ? "".toCharArray() : password.toCharArray());
371
372            try {
373                session = jcrRepository(repositoryId).login(credentials, workspace(repositoryId));
374                sessions.put(repositoryId, session);
375            } catch (Exception e) {
376                throw new RuntimeException(e);
377            }
378        }
379        return session;
380    }
381
382    private JcrRepository jcrRepository(String repositoryId) {
383        String name = name(repositoryId);
384        JcrRepository repo = jcrRepositories.get(name);
385        if (repo == null) {
386            throw new CmisInvalidArgumentException("Repository lookup failed for \"" + repositoryId + "\" (using name \"" + name + "\")");
387        }
388        return repo;
389    }
390
391    private String name(String repositoryId) {
392        CheckArg.isNotNull(repositoryId, "repositoryId"); // if can be user-supplied, or 'assert repositoryId != null' if not user supplied
393        repositoryId = repositoryId.trim();
394        String[] parts = repositoryId.split(":", 2);
395        int numParts = parts.length;
396
397        if (numParts > 1) {
398            return parts[0].trim(); // may be blank
399        }
400        return repositoryId;
401    }
402
403    /**
404     *
405     * @param repositoryId The repositoryId
406     * @return The workspace. If {@code repositoryId} is a single word (i.e. not
407     * colon-separated), then it is assumed that the {@code repositoryId}
408     * argument only represents the repository name and this method will return
409     * {@code null} for the workspace.
410     */
411    private String workspace(String repositoryId) {
412        CheckArg.isNotNull(repositoryId, "repositoryId"); // if can be user-supplied, or 'assert repositoryId != null' if not user supplied
413        repositoryId = repositoryId.trim();
414        String[] parts = repositoryId.split(":");
415        int numParts = parts.length;
416
417        if (numParts > 1) {
418            // Just take the second part
419            return parts[1].trim(); // may be blank
420        }
421        return null;
422    }
423}