001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.cmis;
029
030import static org.opencms.cmis.CmsCmisUtil.addAction;
031import static org.opencms.cmis.CmsCmisUtil.addPropertyDateTime;
032import static org.opencms.cmis.CmsCmisUtil.addPropertyId;
033import static org.opencms.cmis.CmsCmisUtil.addPropertyString;
034import static org.opencms.cmis.CmsCmisUtil.handleCmsException;
035import static org.opencms.cmis.CmsCmisUtil.millisToCalendar;
036
037import org.opencms.file.CmsObject;
038import org.opencms.file.CmsResource;
039import org.opencms.file.CmsResourceFilter;
040import org.opencms.file.CmsUser;
041import org.opencms.lock.CmsLock;
042import org.opencms.main.CmsException;
043import org.opencms.relations.CmsRelation;
044import org.opencms.relations.CmsRelationFilter;
045import org.opencms.relations.CmsRelationType;
046import org.opencms.security.CmsPermissionSet;
047import org.opencms.util.CmsUUID;
048
049import java.util.ArrayList;
050import java.util.GregorianCalendar;
051import java.util.LinkedHashSet;
052import java.util.List;
053import java.util.Set;
054import java.util.regex.Matcher;
055import java.util.regex.Pattern;
056
057import org.apache.chemistry.opencmis.commons.PropertyIds;
058import org.apache.chemistry.opencmis.commons.data.Ace;
059import org.apache.chemistry.opencmis.commons.data.Acl;
060import org.apache.chemistry.opencmis.commons.data.AllowableActions;
061import org.apache.chemistry.opencmis.commons.data.ObjectData;
062import org.apache.chemistry.opencmis.commons.data.Properties;
063import org.apache.chemistry.opencmis.commons.enums.Action;
064import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
065import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
066import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
067import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
068import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
069import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
070import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
071import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
072import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
073import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
074
075/**
076 * Helper class for CMIS CRUD operations on relation objects.<p>
077 *
078 * Since CMIS requires any object to have an ID by which it is accessed, but OpenCms relations
079 * are not addressable by ids, we invent an artificial relation id string of the form
080 * REL_(SOURCE_ID)_(TARGET_ID)_(TYPE).<p>
081 *
082 */
083public class CmsCmisRelationHelper implements I_CmsCmisObjectHelper {
084
085    /**
086     * A class which contains the necessary information to identify a relation object.<p>
087     */
088    public static class RelationKey {
089
090        /** The internal OpenCms relation object (optional). */
091        private CmsRelation m_relation;
092
093        /** The relation type string. */
094        private String m_relType;
095
096        /** The internal OpenCms resource which is the relation source (optional). */
097        private CmsResource m_source;
098
099        /** The source id of the relation. */
100        private CmsUUID m_sourceId;
101
102        /** The target id of the relation. */
103        private CmsUUID m_targetId;
104
105        /**
106         * Creates a new relation key.<p>
107         *
108         * @param sourceId the source id
109         * @param targetId the target id
110         * @param relType the relation type
111         */
112        public RelationKey(CmsUUID sourceId, CmsUUID targetId, String relType) {
113
114            m_sourceId = sourceId;
115            m_targetId = targetId;
116            m_relType = relType;
117        }
118
119        /**
120         * Reads the actual resource and relation data from the OpenCms VFS.<p>
121         *
122         * @param cms the CMS context to use for reading the data
123         */
124        public void fillRelation(CmsObject cms) {
125
126            try {
127                m_source = cms.readResource(m_sourceId);
128                List<CmsRelation> relations = cms.getRelationsForResource(
129                    m_source,
130                    CmsRelationFilter.TARGETS.filterStructureId(m_targetId).filterType(getRelationType(m_relType)));
131                if (relations.isEmpty()) {
132                    throw new CmisObjectNotFoundException(toString());
133                }
134                m_relation = relations.get(0);
135            } catch (CmsException e) {
136                CmsCmisUtil.handleCmsException(e);
137            }
138        }
139
140        /**
141         * Gets the relation object.<p>
142         *
143         * @return the relation object
144         */
145        public CmsRelation getRelation() {
146
147            return m_relation;
148        }
149
150        /**
151         * Gets the relation type.<p>
152         *
153         * @return the relation type
154         */
155        public String getRelType() {
156
157            return m_relType;
158        }
159
160        /**
161         * Gets the source resource of the relation.<p>
162         *
163         * @return the source of the relation
164         */
165        public CmsResource getSource() {
166
167            return m_source;
168        }
169
170        /**
171         * Gets the source id.<p>
172         *
173         * @return the source id
174         */
175        public CmsUUID getSourceId() {
176
177            return m_sourceId;
178        }
179
180        /**
181         * Gets the target id of the relation.<p>
182         *
183         * @return the target id
184         */
185        public CmsUUID getTargetId() {
186
187            return m_targetId;
188        }
189
190        /**
191         * Sets the relation type.<p>
192         *
193         * @param relType the relation type
194         */
195        public void setRelType(String relType) {
196
197            m_relType = relType;
198        }
199
200        /**
201         * Sets the source id.<p>
202         *
203         * @param sourceId the source id
204         */
205        public void setSourceId(CmsUUID sourceId) {
206
207            m_sourceId = sourceId;
208        }
209
210        /**
211         * Sets the target id.<p>
212         *
213         * @param targetId the target id
214         */
215        public void setTargetId(CmsUUID targetId) {
216
217            m_targetId = targetId;
218        }
219
220        /**
221         * @see java.lang.Object#toString()
222         */
223        @Override
224        public String toString() {
225
226            return createKey(m_sourceId, m_targetId, m_relType);
227        }
228    }
229
230    /** The prefix used to identify relation ids. */
231    public static final String RELATION_ID_PREFIX = "REL_";
232
233    /** The pattern which relation ids should match. */
234    public static final Pattern RELATION_PATTERN = Pattern.compile(
235        "^REL_(" + CmsUUID.UUID_REGEX + ")_(" + CmsUUID.UUID_REGEX + ")_(.*)$");
236
237    /** The underlying CMIS repository. */
238    private CmsCmisRepository m_repository;
239
240    /**
241     * Creates a new relation helper for the given repository.<p>
242     *
243     * @param repository the repository
244     */
245    public CmsCmisRelationHelper(CmsCmisRepository repository) {
246
247        m_repository = repository;
248    }
249
250    /**
251     * Creates a relation id string from the source and target ids and a relation type.<p>
252     *
253     * @param source the source id
254     * @param target the target id
255     * @param relType the relation type
256     *
257     * @return the relation id
258     */
259    protected static String createKey(CmsUUID source, CmsUUID target, String relType) {
260
261        return RELATION_ID_PREFIX + source + "_" + target + "_" + relType;
262    }
263
264    /**
265     * Gets a relation type by name.<p>
266     *
267     * @param typeName the relation type name
268     *
269     * @return the relation type with the matching name
270     */
271    protected static CmsRelationType getRelationType(String typeName) {
272
273        for (CmsRelationType relType : CmsRelationType.getAll()) {
274            if (relType.getName().equalsIgnoreCase(typeName)) {
275                return relType;
276            }
277        }
278        return null;
279    }
280
281    /**
282     * @see org.opencms.cmis.I_CmsCmisObjectHelper#deleteObject(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean)
283     */
284    public void deleteObject(CmsCmisCallContext context, String objectId, boolean allVersions) {
285
286        try {
287
288            RelationKey rk = parseRelationKey(objectId);
289            CmsUUID sourceId = rk.getSourceId();
290            CmsObject cms = m_repository.getCmsObject(context);
291            CmsResource sourceResource = cms.readResource(sourceId);
292            boolean wasLocked = CmsCmisUtil.ensureLock(cms, sourceResource);
293            try {
294                CmsRelationFilter relFilter = CmsRelationFilter.ALL.filterType(
295                    getRelationType(rk.getRelType())).filterStructureId(rk.getTargetId());
296                cms.deleteRelationsFromResource(sourceResource.getRootPath(), relFilter);
297            } finally {
298                if (wasLocked) {
299                    cms.unlockResource(sourceResource);
300                }
301            }
302        } catch (CmsException e) {
303            CmsCmisUtil.handleCmsException(e);
304        }
305    }
306
307    /**
308     * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAcl(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean)
309     */
310    public Acl getAcl(CmsCmisCallContext context, String objectId, boolean onlyBasicPermissions) {
311
312        CmsObject cms = m_repository.getCmsObject(context);
313        RelationKey rk = parseRelationKey(objectId);
314        rk.fillRelation(cms);
315        return collectAcl(cms, rk.getSource(), onlyBasicPermissions);
316    }
317
318    /**
319     * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAllowableActions(org.opencms.cmis.CmsCmisCallContext, java.lang.String)
320     */
321    public AllowableActions getAllowableActions(CmsCmisCallContext context, String objectId) {
322
323        CmsObject cms = m_repository.getCmsObject(context);
324        RelationKey rk = parseRelationKey(objectId);
325        rk.fillRelation(cms);
326        return collectAllowableActions(cms, rk.getSource(), rk.getRelation());
327    }
328
329    /**
330     * @see org.opencms.cmis.I_CmsCmisObjectHelper#getObject(org.opencms.cmis.CmsCmisCallContext, java.lang.String, java.lang.String, boolean, org.apache.chemistry.opencmis.commons.enums.IncludeRelationships, java.lang.String, boolean, boolean)
331     */
332    public ObjectData getObject(
333        CmsCmisCallContext context,
334        String objectId,
335        String filter,
336        boolean includeAllowableActions,
337        IncludeRelationships includeRelationships,
338        String renditionFilter,
339        boolean includePolicyIds,
340        boolean includeAcl) {
341
342        CmsObject cms = m_repository.getCmsObject(context);
343        RelationKey rk = parseRelationKey(objectId);
344        rk.fillRelation(cms);
345        Set<String> filterSet = CmsCmisUtil.splitFilter(filter);
346        ObjectData result = collectObjectData(
347            context,
348            cms,
349            rk.getSource(),
350            rk.getRelation(),
351            filterSet,
352            includeAllowableActions,
353            includeAcl);
354        return result;
355    }
356
357    /**
358     * Compiles the ACL for a relation.<p>
359     *
360     * @param cms the CMS context
361     * @param resource the resource for which to collect the ACLs
362     * @param onlyBasic flag to only include basic ACEs
363     *
364     * @return the ACL for the resource
365     */
366    protected Acl collectAcl(CmsObject cms, CmsResource resource, boolean onlyBasic) {
367
368        AccessControlListImpl cmisAcl = new AccessControlListImpl();
369        List<Ace> cmisAces = new ArrayList<Ace>();
370        cmisAcl.setAces(cmisAces);
371        cmisAcl.setExact(Boolean.FALSE);
372        return cmisAcl;
373    }
374
375    /**
376     * Collects the allowable actions for a relation.<p>
377     *
378     * @param cms the current CMS context
379     * @param file the source of the relation
380     * @param relation the relation object
381     *
382     * @return the allowable actions for the given resource
383     */
384    protected AllowableActions collectAllowableActions(CmsObject cms, CmsResource file, CmsRelation relation) {
385
386        try {
387            Set<Action> aas = new LinkedHashSet<Action>();
388            AllowableActionsImpl result = new AllowableActionsImpl();
389
390            CmsLock lock = cms.getLock(file);
391            CmsUser user = cms.getRequestContext().getCurrentUser();
392            boolean canWrite = !cms.getRequestContext().getCurrentProject().isOnlineProject()
393                && (lock.isOwnedBy(user) || lock.isLockableBy(user))
394                && cms.hasPermissions(file, CmsPermissionSet.ACCESS_WRITE, false, CmsResourceFilter.DEFAULT);
395            addAction(aas, Action.CAN_GET_PROPERTIES, true);
396            addAction(aas, Action.CAN_DELETE_OBJECT, canWrite && !relation.getType().isDefinedInContent());
397            result.setAllowableActions(aas);
398            return result;
399        } catch (CmsException e) {
400            handleCmsException(e);
401            return null;
402        }
403    }
404
405    /**
406     * Fills in an ObjectData record.<p>
407     *
408     * @param context the call context
409     * @param cms the CMS context
410     * @param resource the resource for which we want the ObjectData
411     * @param relation the relation object
412     * @param filter the property filter string
413     * @param includeAllowableActions true if the allowable actions should be included
414     * @param includeAcl true if the ACL entries should be included
415     *
416     * @return the object data
417     */
418    protected ObjectData collectObjectData(
419        CmsCmisCallContext context,
420        CmsObject cms,
421        CmsResource resource,
422        CmsRelation relation,
423        Set<String> filter,
424        boolean includeAllowableActions,
425        boolean includeAcl) {
426
427        ObjectDataImpl result = new ObjectDataImpl();
428        ObjectInfoImpl objectInfo = new ObjectInfoImpl();
429
430        result.setProperties(collectProperties(cms, resource, relation, filter, objectInfo));
431
432        if (includeAllowableActions) {
433            result.setAllowableActions(collectAllowableActions(cms, resource, relation));
434        }
435
436        if (includeAcl) {
437            result.setAcl(collectAcl(cms, resource, true));
438            result.setIsExactAcl(Boolean.FALSE);
439        }
440
441        if (context.isObjectInfoRequired()) {
442            objectInfo.setObject(result);
443            context.getObjectInfoHandler().addObjectInfo(objectInfo);
444        }
445        return result;
446    }
447
448    /**
449     * Gathers all base properties of a file or folder.
450     *
451     * @param cms the current CMS context
452     * @param resource the file for which we want the properties
453     * @param relation the relation object
454     * @param orgfilter the property filter
455     * @param objectInfo the object info handler
456     *
457     * @return the properties for the given resource
458     */
459    protected Properties collectProperties(
460        CmsObject cms,
461        CmsResource resource,
462        CmsRelation relation,
463        Set<String> orgfilter,
464        ObjectInfoImpl objectInfo) {
465
466        CmsCmisTypeManager tm = m_repository.getTypeManager();
467
468        if (resource == null) {
469            throw new IllegalArgumentException("Resource may not be null.");
470        }
471
472        // copy filter
473        Set<String> filter = (orgfilter == null ? null : new LinkedHashSet<String>(orgfilter));
474
475        // find base type
476        String typeId = "opencms:" + relation.getType().getName();
477        objectInfo.setBaseType(BaseTypeId.CMIS_RELATIONSHIP);
478        objectInfo.setTypeId(typeId);
479        objectInfo.setContentType(null);
480        objectInfo.setFileName(null);
481        objectInfo.setHasAcl(false);
482        objectInfo.setHasContent(false);
483        objectInfo.setVersionSeriesId(null);
484        objectInfo.setIsCurrentVersion(true);
485        objectInfo.setRelationshipSourceIds(null);
486        objectInfo.setRelationshipTargetIds(null);
487        objectInfo.setRenditionInfos(null);
488        objectInfo.setSupportsDescendants(false);
489        objectInfo.setSupportsFolderTree(false);
490        objectInfo.setSupportsPolicies(false);
491        objectInfo.setSupportsRelationships(false);
492        objectInfo.setWorkingCopyId(null);
493        objectInfo.setWorkingCopyOriginalId(null);
494
495        // let's do it
496        try {
497            PropertiesImpl result = new PropertiesImpl();
498
499            // id
500            String id = createKey(relation);
501            addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_ID, id);
502            objectInfo.setId(id);
503
504            // name
505            String name = createReadableName(relation);
506            addPropertyString(tm, result, typeId, filter, PropertyIds.NAME, name);
507            objectInfo.setName(name);
508
509            // created and modified by
510            CmsUUID creatorId = resource.getUserCreated();
511            CmsUUID modifierId = resource.getUserLastModified();
512            String creatorName = creatorId.toString();
513            String modifierName = modifierId.toString();
514            try {
515                CmsUser user = cms.readUser(creatorId);
516                creatorName = user.getName();
517            } catch (CmsException e) {
518                // ignore, use id as name
519            }
520            try {
521                CmsUser user = cms.readUser(modifierId);
522                modifierName = user.getName();
523            } catch (CmsException e) {
524                // ignore, use id as name
525            }
526
527            addPropertyString(tm, result, typeId, filter, PropertyIds.CREATED_BY, creatorName);
528            addPropertyString(tm, result, typeId, filter, PropertyIds.LAST_MODIFIED_BY, modifierName);
529            objectInfo.setCreatedBy(creatorName);
530
531            addPropertyId(tm, result, typeId, filter, PropertyIds.SOURCE_ID, relation.getSourceId().toString());
532            addPropertyId(tm, result, typeId, filter, PropertyIds.TARGET_ID, relation.getTargetId().toString());
533
534            // creation and modification date
535            GregorianCalendar lastModified = millisToCalendar(resource.getDateLastModified());
536            GregorianCalendar created = millisToCalendar(resource.getDateCreated());
537
538            addPropertyDateTime(tm, result, typeId, filter, PropertyIds.CREATION_DATE, created);
539            addPropertyDateTime(tm, result, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified);
540            objectInfo.setCreationDate(created);
541            objectInfo.setLastModificationDate(lastModified);
542
543            // change token - always null
544            addPropertyString(tm, result, typeId, filter, PropertyIds.CHANGE_TOKEN, null);
545
546            // base type and type name
547            addPropertyId(tm, result, typeId, filter, PropertyIds.BASE_TYPE_ID, BaseTypeId.CMIS_RELATIONSHIP.value());
548            addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_TYPE_ID, typeId);
549            objectInfo.setHasParent(false);
550            return result;
551        } catch (Exception e) {
552            if (e instanceof CmisBaseException) {
553                throw (CmisBaseException)e;
554            }
555            throw new CmisRuntimeException(e.getMessage(), e);
556        }
557    }
558
559    /**
560     * Creates a user-readable name from the given relation object.<p>
561     *
562     * @param relation the relation object
563     *
564     * @return the readable name
565     */
566    protected String createReadableName(CmsRelation relation) {
567
568        return relation.getType().getName()
569            + "[ "
570            + relation.getSourcePath()
571            + " -> "
572            + relation.getTargetPath()
573            + " ]";
574    }
575
576    /**
577     * Extracts the source/target ids and the type from a relation id.<p>
578     *
579     * @param id the relation id
580     *
581     * @return the relation key object
582     */
583    protected RelationKey parseRelationKey(String id) {
584
585        Matcher matcher = RELATION_PATTERN.matcher(id);
586        matcher.find();
587        CmsUUID src = new CmsUUID(matcher.group(1));
588        CmsUUID tgt = new CmsUUID(matcher.group(2));
589        String tp = matcher.group(3);
590        return new RelationKey(src, tgt, tp);
591    }
592
593    /**
594     * Creates a relation id from the given OpenCms relation object.<p>
595     *
596     * @param relation the OpenCms relation object
597     *
598     * @return the relation id
599     */
600    String createKey(CmsRelation relation) {
601
602        return createKey(relation.getSourceId(), relation.getTargetId(), relation.getType().getName());
603    }
604
605}