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("^REL_("
235        + CmsUUID.UUID_REGEX
236        + ")_("
237        + CmsUUID.UUID_REGEX
238        + ")_(.*)$");
239
240    /** The underlying CMIS repository. */
241    private CmsCmisRepository m_repository;
242
243    /**
244     * Creates a new relation helper for the given repository.<p>
245     * 
246     * @param repository the repository 
247     */
248    public CmsCmisRelationHelper(CmsCmisRepository repository) {
249
250        m_repository = repository;
251    }
252
253    /** 
254     * Creates a relation id string from the source and target ids and a relation type.<p>
255     *  
256     * @param source the source id 
257     * @param target the target id 
258     * @param relType the relation type 
259     * 
260     * @return the relation id 
261     */
262    protected static String createKey(CmsUUID source, CmsUUID target, String relType) {
263
264        return RELATION_ID_PREFIX + source + "_" + target + "_" + relType;
265    }
266
267    /**
268     * Gets a relation type by name.<p>
269     * 
270     * @param typeName the relation type name 
271     * 
272     * @return the relation type with the matching name 
273     */
274    protected static CmsRelationType getRelationType(String typeName) {
275
276        for (CmsRelationType relType : CmsRelationType.getAll()) {
277            if (relType.getName().equalsIgnoreCase(typeName)) {
278                return relType;
279            }
280        }
281        return null;
282    }
283
284    /**
285     * @see org.opencms.cmis.I_CmsCmisObjectHelper#deleteObject(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean)
286     */
287    public void deleteObject(CmsCmisCallContext context, String objectId, boolean allVersions) {
288
289        try {
290
291            RelationKey rk = parseRelationKey(objectId);
292            CmsUUID sourceId = rk.getSourceId();
293            CmsObject cms = m_repository.getCmsObject(context);
294            CmsResource sourceResource = cms.readResource(sourceId);
295            boolean wasLocked = CmsCmisUtil.ensureLock(cms, sourceResource);
296            try {
297                CmsRelationFilter relFilter = CmsRelationFilter.ALL.filterType(getRelationType(rk.getRelType())).filterStructureId(
298                    rk.getTargetId());
299                cms.deleteRelationsFromResource(sourceResource.getRootPath(), relFilter);
300            } finally {
301                if (wasLocked) {
302                    cms.unlockResource(sourceResource);
303                }
304            }
305        } catch (CmsException e) {
306            CmsCmisUtil.handleCmsException(e);
307        }
308    }
309
310    /**
311     * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAcl(org.opencms.cmis.CmsCmisCallContext, java.lang.String, boolean)
312     */
313    public Acl getAcl(CmsCmisCallContext context, String objectId, boolean onlyBasicPermissions) {
314
315        CmsObject cms = m_repository.getCmsObject(context);
316        RelationKey rk = parseRelationKey(objectId);
317        rk.fillRelation(cms);
318        return collectAcl(cms, rk.getSource(), onlyBasicPermissions);
319    }
320
321    /**
322     * @see org.opencms.cmis.I_CmsCmisObjectHelper#getAllowableActions(org.opencms.cmis.CmsCmisCallContext, java.lang.String)
323     */
324    public AllowableActions getAllowableActions(CmsCmisCallContext context, String objectId) {
325
326        CmsObject cms = m_repository.getCmsObject(context);
327        RelationKey rk = parseRelationKey(objectId);
328        rk.fillRelation(cms);
329        return collectAllowableActions(cms, rk.getSource(), rk.getRelation());
330    }
331
332    /**
333     * @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)
334     */
335    public ObjectData getObject(
336        CmsCmisCallContext context,
337        String objectId,
338        String filter,
339        boolean includeAllowableActions,
340        IncludeRelationships includeRelationships,
341        String renditionFilter,
342        boolean includePolicyIds,
343        boolean includeAcl) {
344
345        CmsObject cms = m_repository.getCmsObject(context);
346        RelationKey rk = parseRelationKey(objectId);
347        rk.fillRelation(cms);
348        Set<String> filterSet = CmsCmisUtil.splitFilter(filter);
349        ObjectData result = collectObjectData(
350            context,
351            cms,
352            rk.getSource(),
353            rk.getRelation(),
354            filterSet,
355            includeAllowableActions,
356            includeAcl);
357        return result;
358    }
359
360    /**
361     * Compiles the ACL for a relation.<p>
362     *  
363     * @param cms the CMS context
364     * @param resource the resource for which to collect the ACLs 
365     * @param onlyBasic flag to only include basic ACEs   
366     * 
367     * @return the ACL for the resource
368     */
369    protected Acl collectAcl(CmsObject cms, CmsResource resource, boolean onlyBasic) {
370
371        AccessControlListImpl cmisAcl = new AccessControlListImpl();
372        List<Ace> cmisAces = new ArrayList<Ace>();
373        cmisAcl.setAces(cmisAces);
374        cmisAcl.setExact(Boolean.FALSE);
375        return cmisAcl;
376    }
377
378    /**
379     * Collects the allowable actions for a relation.<p>
380     *  
381     * @param cms the current CMS context 
382     * @param file the source of the relation 
383     * @param relation the relation object  
384     * 
385     * @return the allowable actions for the given resource 
386     */
387    protected AllowableActions collectAllowableActions(CmsObject cms, CmsResource file, CmsRelation relation) {
388
389        try {
390            Set<Action> aas = new LinkedHashSet<Action>();
391            AllowableActionsImpl result = new AllowableActionsImpl();
392
393            CmsLock lock = cms.getLock(file);
394            CmsUser user = cms.getRequestContext().getCurrentUser();
395            boolean canWrite = !cms.getRequestContext().getCurrentProject().isOnlineProject()
396                && (lock.isOwnedBy(user) || lock.isLockableBy(user))
397                && cms.hasPermissions(file, CmsPermissionSet.ACCESS_WRITE, false, CmsResourceFilter.DEFAULT);
398            addAction(aas, Action.CAN_GET_PROPERTIES, true);
399            addAction(aas, Action.CAN_DELETE_OBJECT, canWrite && !relation.getType().isDefinedInContent());
400            result.setAllowableActions(aas);
401            return result;
402        } catch (CmsException e) {
403            handleCmsException(e);
404            return null;
405        }
406    }
407
408    /**
409     * Fills in an ObjectData record.<p>
410     * 
411     * @param context the call context
412     * @param cms the CMS context
413     * @param resource the resource for which we want the ObjectData
414     * @param relation the relation object 
415     * @param filter the property filter string 
416     * @param includeAllowableActions true if the allowable actions should be included  
417     * @param includeAcl true if the ACL entries should be included
418     * 
419     * @return the object data 
420     */
421    protected ObjectData collectObjectData(
422        CmsCmisCallContext context,
423        CmsObject cms,
424        CmsResource resource,
425        CmsRelation relation,
426        Set<String> filter,
427        boolean includeAllowableActions,
428        boolean includeAcl) {
429
430        ObjectDataImpl result = new ObjectDataImpl();
431        ObjectInfoImpl objectInfo = new ObjectInfoImpl();
432
433        result.setProperties(collectProperties(cms, resource, relation, filter, objectInfo));
434
435        if (includeAllowableActions) {
436            result.setAllowableActions(collectAllowableActions(cms, resource, relation));
437        }
438
439        if (includeAcl) {
440            result.setAcl(collectAcl(cms, resource, true));
441            result.setIsExactAcl(Boolean.FALSE);
442        }
443
444        if (context.isObjectInfoRequired()) {
445            objectInfo.setObject(result);
446            context.getObjectInfoHandler().addObjectInfo(objectInfo);
447        }
448        return result;
449    }
450
451    /**
452     * Gathers all base properties of a file or folder. 
453     * 
454     * @param cms the current CMS context 
455     * @param resource the file for which we want the properties 
456     * @param relation the relation object  
457     * @param orgfilter the property filter 
458     * @param objectInfo the object info handler 
459     * 
460     * @return the properties for the given resource 
461     */
462    protected Properties collectProperties(
463        CmsObject cms,
464        CmsResource resource,
465        CmsRelation relation,
466        Set<String> orgfilter,
467        ObjectInfoImpl objectInfo) {
468
469        CmsCmisTypeManager tm = m_repository.getTypeManager();
470
471        if (resource == null) {
472            throw new IllegalArgumentException("Resource may not be null.");
473        }
474
475        // copy filter
476        Set<String> filter = (orgfilter == null ? null : new LinkedHashSet<String>(orgfilter));
477
478        // find base type
479        String typeId = "opencms:" + relation.getType().getName();
480        objectInfo.setBaseType(BaseTypeId.CMIS_RELATIONSHIP);
481        objectInfo.setTypeId(typeId);
482        objectInfo.setContentType(null);
483        objectInfo.setFileName(null);
484        objectInfo.setHasAcl(false);
485        objectInfo.setHasContent(false);
486        objectInfo.setVersionSeriesId(null);
487        objectInfo.setIsCurrentVersion(true);
488        objectInfo.setRelationshipSourceIds(null);
489        objectInfo.setRelationshipTargetIds(null);
490        objectInfo.setRenditionInfos(null);
491        objectInfo.setSupportsDescendants(false);
492        objectInfo.setSupportsFolderTree(false);
493        objectInfo.setSupportsPolicies(false);
494        objectInfo.setSupportsRelationships(false);
495        objectInfo.setWorkingCopyId(null);
496        objectInfo.setWorkingCopyOriginalId(null);
497
498        // let's do it
499        try {
500            PropertiesImpl result = new PropertiesImpl();
501
502            // id
503            String id = createKey(relation);
504            addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_ID, id);
505            objectInfo.setId(id);
506
507            // name
508            String name = createReadableName(relation);
509            addPropertyString(tm, result, typeId, filter, PropertyIds.NAME, name);
510            objectInfo.setName(name);
511
512            // created and modified by
513            CmsUUID creatorId = resource.getUserCreated();
514            CmsUUID modifierId = resource.getUserLastModified();
515            String creatorName = creatorId.toString();
516            String modifierName = modifierId.toString();
517            try {
518                CmsUser user = cms.readUser(creatorId);
519                creatorName = user.getName();
520            } catch (CmsException e) {
521                // ignore, use id as name 
522            }
523            try {
524                CmsUser user = cms.readUser(modifierId);
525                modifierName = user.getName();
526            } catch (CmsException e) {
527                // ignore, use id as name
528            }
529
530            addPropertyString(tm, result, typeId, filter, PropertyIds.CREATED_BY, creatorName);
531            addPropertyString(tm, result, typeId, filter, PropertyIds.LAST_MODIFIED_BY, modifierName);
532            objectInfo.setCreatedBy(creatorName);
533
534            addPropertyId(tm, result, typeId, filter, PropertyIds.SOURCE_ID, relation.getSourceId().toString());
535            addPropertyId(tm, result, typeId, filter, PropertyIds.TARGET_ID, relation.getTargetId().toString());
536
537            // creation and modification date
538            GregorianCalendar lastModified = millisToCalendar(resource.getDateLastModified());
539            GregorianCalendar created = millisToCalendar(resource.getDateCreated());
540
541            addPropertyDateTime(tm, result, typeId, filter, PropertyIds.CREATION_DATE, created);
542            addPropertyDateTime(tm, result, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified);
543            objectInfo.setCreationDate(created);
544            objectInfo.setLastModificationDate(lastModified);
545
546            // change token - always null
547            addPropertyString(tm, result, typeId, filter, PropertyIds.CHANGE_TOKEN, null);
548
549            // base type and type name
550            addPropertyId(tm, result, typeId, filter, PropertyIds.BASE_TYPE_ID, BaseTypeId.CMIS_RELATIONSHIP.value());
551            addPropertyId(tm, result, typeId, filter, PropertyIds.OBJECT_TYPE_ID, typeId);
552            objectInfo.setHasParent(false);
553            return result;
554        } catch (Exception e) {
555            if (e instanceof CmisBaseException) {
556                throw (CmisBaseException)e;
557            }
558            throw new CmisRuntimeException(e.getMessage(), e);
559        }
560    }
561
562    /**
563     * Creates a user-readable name from the given relation object.<p>
564     * 
565     * @param relation the relation object 
566     * 
567     * @return the readable name 
568     */
569    protected String createReadableName(CmsRelation relation) {
570
571        return relation.getType().getName()
572            + "[ "
573            + relation.getSourcePath()
574            + " -> "
575            + relation.getTargetPath()
576            + " ]";
577    }
578
579    /**
580     * Extracts the source/target ids and the type from a relation id.<p>
581     * 
582     * @param id the relation id 
583     * 
584     * @return the relation key object
585     */
586    protected RelationKey parseRelationKey(String id) {
587
588        Matcher matcher = RELATION_PATTERN.matcher(id);
589        matcher.find();
590        CmsUUID src = new CmsUUID(matcher.group(1));
591        CmsUUID tgt = new CmsUUID(matcher.group(2));
592        String tp = matcher.group(3);
593        return new RelationKey(src, tgt, tp);
594    }
595
596    /**
597     * Creates a relation id from the given OpenCms relation object.<p>
598     * 
599     * @param relation the OpenCms relation object
600     *  
601     * @return the relation id 
602     */
603    String createKey(CmsRelation relation) {
604
605        return createKey(relation.getSourceId(), relation.getTargetId(), relation.getType().getName());
606    }
607
608}