View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.chemistry.opencmis.client.bindings.spi.atompub;
20  
21  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNotEmpty;
22  
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.math.BigInteger;
26  import java.util.ArrayList;
27  import java.util.HashMap;
28  import java.util.HashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import javax.xml.stream.XMLStreamWriter;
34  
35  import org.apache.chemistry.opencmis.client.bindings.impl.CmisBindingsHelper;
36  import org.apache.chemistry.opencmis.client.bindings.impl.RepositoryInfoCache;
37  import org.apache.chemistry.opencmis.client.bindings.spi.BindingSession;
38  import org.apache.chemistry.opencmis.client.bindings.spi.LinkAccess;
39  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomAcl;
40  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomBase;
41  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomElement;
42  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomEntry;
43  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomLink;
44  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.RepositoryWorkspace;
45  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.ServiceDoc;
46  import org.apache.chemistry.opencmis.client.bindings.spi.http.HttpInvoker;
47  import org.apache.chemistry.opencmis.client.bindings.spi.http.Output;
48  import org.apache.chemistry.opencmis.client.bindings.spi.http.Response;
49  import org.apache.chemistry.opencmis.commons.PropertyIds;
50  import org.apache.chemistry.opencmis.commons.SessionParameter;
51  import org.apache.chemistry.opencmis.commons.data.Ace;
52  import org.apache.chemistry.opencmis.commons.data.Acl;
53  import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
54  import org.apache.chemistry.opencmis.commons.data.ObjectData;
55  import org.apache.chemistry.opencmis.commons.data.Properties;
56  import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
57  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
58  import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
59  import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
60  import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
61  import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
62  import org.apache.chemistry.opencmis.commons.exceptions.CmisConnectionException;
63  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
64  import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
65  import org.apache.chemistry.opencmis.commons.exceptions.CmisFilterNotValidException;
66  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
67  import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException;
68  import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
69  import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
70  import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
71  import org.apache.chemistry.opencmis.commons.exceptions.CmisProxyAuthenticationException;
72  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
73  import org.apache.chemistry.opencmis.commons.exceptions.CmisServiceUnavailableException;
74  import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException;
75  import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
76  import org.apache.chemistry.opencmis.commons.exceptions.CmisTooManyRequestsException;
77  import org.apache.chemistry.opencmis.commons.exceptions.CmisUnauthorizedException;
78  import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
79  import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException;
80  import org.apache.chemistry.opencmis.commons.impl.Constants;
81  import org.apache.chemistry.opencmis.commons.impl.IOUtils;
82  import org.apache.chemistry.opencmis.commons.impl.ReturnVersion;
83  import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
84  import org.apache.chemistry.opencmis.commons.impl.XMLConverter;
85  import org.apache.chemistry.opencmis.commons.impl.XMLUtils;
86  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlEntryImpl;
87  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
88  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl;
89  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
90  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PolicyIdListImpl;
91  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
92  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
93  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
94  
95  /**
96   * Base class for all AtomPub client services.
97   */
98  public abstract class AbstractAtomPubService implements LinkAccess {
99  
100     protected enum IdentifierType {
101         ID, PATH
102     }
103 
104     protected static final String NAME_COLLECTION = "collection";
105     protected static final String NAME_URI_TEMPLATE = "uritemplate";
106     protected static final String NAME_PATH_SEGMENT = "pathSegment";
107     protected static final String NAME_RELATIVE_PATH_SEGMENT = "relativePathSegment";
108     protected static final String NAME_NUM_ITEMS = "numItems";
109 
110     private static final String EXCEPTION_EXCEPTION_BEGIN = "<!--exception-->";
111     private static final String EXCEPTION_EXCEPTION_END = "<!--/exception-->";
112     private static final String EXCEPTION_MESSAGE_BEGIN = "<!--message-->";
113     private static final String EXCEPTION_MESSAGE_END = "<!--/message-->";
114     private static final String EXCEPTION_KEY_BEGIN = "<!--key-->";
115     private static final String EXCEPTION_KEY_END = "<!--/key-->";
116     private static final String EXCEPTION_VALUE_BEGIN = "<!--value-->";
117     private static final String EXCEPTION_VALUE_END = "<!--/value-->";
118 
119     private BindingSession session;
120 
121     /**
122      * Sets the current session.
123      */
124     protected void setSession(BindingSession session) {
125         this.session = session;
126     }
127 
128     /**
129      * Gets the current session.
130      */
131     protected BindingSession getSession() {
132         return session;
133     }
134 
135     /**
136      * Gets the HTTP Invoker object.
137      */
138     protected HttpInvoker getHttpInvoker() {
139         return CmisBindingsHelper.getHttpInvoker(session);
140     }
141 
142     /**
143      * Returns the service document URL of this session.
144      */
145     protected String getServiceDocURL() {
146         Object url = session.get(SessionParameter.ATOMPUB_URL);
147         if (url instanceof String) {
148             return (String) url;
149         }
150 
151         return null;
152     }
153 
154     /**
155      * Return the CMIS version of the given repository.
156      */
157     protected CmisVersion getCmisVersion(String repositoryId) {
158         if (CmisBindingsHelper.getForcedCmisVersion(session) != null) {
159             return CmisBindingsHelper.getForcedCmisVersion(session);
160         }
161 
162         RepositoryInfoCache cache = CmisBindingsHelper.getRepositoryInfoCache(session);
163         RepositoryInfo info = cache.get(repositoryId);
164 
165         if (info == null) {
166             List<RepositoryInfo> infoList = getRepositoriesInternal(repositoryId);
167             if (isNotEmpty(infoList)) {
168                 info = infoList.get(0);
169                 cache.put(info);
170             }
171         }
172 
173         return info == null ? CmisVersion.CMIS_1_0 : info.getCmisVersion();
174     }
175 
176     // ---- link cache ----
177 
178     /**
179      * Returns the link cache or creates a new cache if it doesn't exist.
180      */
181     protected LinkCache getLinkCache() {
182         LinkCache linkCache = (LinkCache) getSession().get(SpiSessionParameter.LINK_CACHE);
183         if (linkCache == null) {
184             linkCache = new LinkCache(getSession());
185             getSession().put(SpiSessionParameter.LINK_CACHE, linkCache);
186         }
187 
188         return linkCache;
189     }
190 
191     /**
192      * Gets a link from the cache.
193      */
194     protected String getLink(String repositoryId, String id, String rel, String type) {
195         if (repositoryId == null) {
196             throw new CmisInvalidArgumentException("Repository ID must be set!");
197         }
198 
199         if (id == null) {
200             throw new CmisInvalidArgumentException("Object ID must be set!");
201         }
202 
203         return getLinkCache().getLink(repositoryId, id, rel, type);
204     }
205 
206     /**
207      * Gets a link from the cache.
208      */
209     protected String getLink(String repositoryId, String id, String rel) {
210         return getLink(repositoryId, id, rel, null);
211     }
212 
213     /**
214      * Gets a link from the cache if it is there or loads it into the cache if
215      * it is not there.
216      */
217     @Override
218     public String loadLink(String repositoryId, String id, String rel, String type) {
219         String link = getLink(repositoryId, id, rel, type);
220         if (link == null) {
221             getObjectInternal(repositoryId, IdentifierType.ID, id, ReturnVersion.THIS, "cmis:objectId", Boolean.FALSE,
222                     IncludeRelationships.NONE, "cmis:none", Boolean.FALSE, Boolean.FALSE, null);
223             link = getLink(repositoryId, id, rel, type);
224         }
225 
226         return link;
227     }
228 
229     /**
230      * Gets the content link from the cache if it is there or loads it into the
231      * cache if it is not there.
232      */
233     @Override
234     public String loadContentLink(String repositoryId, String id) {
235         return loadLink(repositoryId, id, AtomPubParser.LINK_REL_CONTENT, null);
236     }
237 
238     /**
239      * Gets a rendition content link from the cache if it is there or loads it
240      * into the cache if it is not there.
241      */
242     @Override
243     public String loadRenditionContentLink(String repositoryId, String id, String streamId) {
244         return loadLink(repositoryId, id, Constants.REL_ALTERNATE, streamId);
245     }
246 
247     /**
248      * Adds a link to the cache.
249      */
250     protected void addLink(String repositoryId, String id, String rel, String type, String link) {
251         getLinkCache().addLink(repositoryId, id, rel, type, link);
252     }
253 
254     /**
255      * Adds a link to the cache.
256      */
257     protected void addLink(String repositoryId, String id, AtomLink link) {
258         getLinkCache().addLink(repositoryId, id, link.getRel(), link.getType(), link.getHref());
259     }
260 
261     /**
262      * Removes all links of an object.
263      */
264     protected void removeLinks(String repositoryId, String id) {
265         getLinkCache().removeLinks(repositoryId, id);
266     }
267 
268     /**
269      * Locks the link cache.
270      */
271     protected void lockLinks() {
272         getLinkCache().lockLinks();
273     }
274 
275     /**
276      * Unlocks the link cache.
277      */
278     protected void unlockLinks() {
279         getLinkCache().unlockLinks();
280     }
281 
282     /**
283      * Checks a link throw an appropriate exception.
284      */
285     protected void throwLinkException(String repositoryId, String id, String rel, String type) {
286         int index = getLinkCache().checkLink(repositoryId, id, rel, type);
287 
288         switch (index) {
289         case 0:
290             throw new CmisObjectNotFoundException("Unknown repository!");
291         case 1:
292             throw new CmisObjectNotFoundException("Unknown object!");
293         case 2:
294             throw new CmisNotSupportedException("Operation not supported by the repository for this object!");
295         case 3:
296             throw new CmisNotSupportedException("No link with matching media type!");
297         case 4:
298             throw new CmisRuntimeException("Nothing wrong! Either this is a bug or a threading issue.");
299         default:
300             throw new CmisRuntimeException("Unknown error!");
301         }
302     }
303 
304     /**
305      * Gets a type link from the cache.
306      */
307     protected String getTypeLink(String repositoryId, String typeId, String rel, String type) {
308         if (repositoryId == null) {
309             throw new CmisInvalidArgumentException("Repository ID must be set!");
310         }
311 
312         if (typeId == null) {
313             throw new CmisInvalidArgumentException("Type ID must be set!");
314         }
315 
316         return getLinkCache().getTypeLink(repositoryId, typeId, rel, type);
317     }
318 
319     /**
320      * Gets a type link from the cache.
321      */
322     protected String getTypeLink(String repositoryId, String typeId, String rel) {
323         return getTypeLink(repositoryId, typeId, rel, null);
324     }
325 
326     /**
327      * Gets a link from the cache if it is there or loads it into the cache if
328      * it is not there.
329      */
330     protected String loadTypeLink(String repositoryId, String typeId, String rel, String type) {
331         String link = getTypeLink(repositoryId, typeId, rel, type);
332         if (link == null) {
333             getTypeDefinitionInternal(repositoryId, typeId);
334             link = getTypeLink(repositoryId, typeId, rel, type);
335         }
336 
337         return link;
338     }
339 
340     /**
341      * Adds a type link to the cache.
342      */
343     protected void addTypeLink(String repositoryId, String typeId, String rel, String type, String link) {
344         getLinkCache().addTypeLink(repositoryId, typeId, rel, type, link);
345     }
346 
347     /**
348      * Adds a type link to the cache.
349      */
350     protected void addTypeLink(String repositoryId, String typeId, AtomLink link) {
351         getLinkCache().addTypeLink(repositoryId, typeId, link.getRel(), link.getType(), link.getHref());
352     }
353 
354     /**
355      * Removes all links of a type.
356      */
357     protected void removeTypeLinks(String repositoryId, String id) {
358         getLinkCache().removeTypeLinks(repositoryId, id);
359     }
360 
361     /**
362      * Locks the type link cache.
363      */
364     protected void lockTypeLinks() {
365         getLinkCache().lockTypeLinks();
366     }
367 
368     /**
369      * Unlocks the type link cache.
370      */
371     protected void unlockTypeLinks() {
372         getLinkCache().unlockTypeLinks();
373     }
374 
375     /**
376      * Gets a collection from the cache.
377      */
378     protected String getCollection(String repositoryId, String collection) {
379         return getLinkCache().getCollection(repositoryId, collection);
380     }
381 
382     /**
383      * Gets a collection from the cache if it is there or loads it into the
384      * cache if it is not there.
385      */
386     protected String loadCollection(String repositoryId, String collection) {
387         String link = getCollection(repositoryId, collection);
388         if (link == null) {
389             // cache repository info
390             getRepositoriesInternal(repositoryId);
391             link = getCollection(repositoryId, collection);
392         }
393 
394         return link;
395     }
396 
397     /**
398      * Adds a collection to the cache.
399      */
400     protected void addCollection(String repositoryId, String collection, String link) {
401         getLinkCache().addCollection(repositoryId, collection, link);
402     }
403 
404     /**
405      * Gets a repository link from the cache.
406      */
407     protected String getRepositoryLink(String repositoryId, String rel) {
408         return getLinkCache().getRepositoryLink(repositoryId, rel);
409     }
410 
411     /**
412      * Gets a repository link from the cache if it is there or loads it into the
413      * cache if it is not there.
414      */
415     protected String loadRepositoryLink(String repositoryId, String rel) {
416         String link = getRepositoryLink(repositoryId, rel);
417         if (link == null) {
418             // cache repository info
419             getRepositoriesInternal(repositoryId);
420             link = getRepositoryLink(repositoryId, rel);
421         }
422 
423         return link;
424     }
425 
426     /**
427      * Adds a repository link to the cache.
428      */
429     protected void addRepositoryLink(String repositoryId, String rel, String link) {
430         getLinkCache().addRepositoryLink(repositoryId, rel, link);
431     }
432 
433     /**
434      * Adds a repository link to the cache.
435      */
436     protected void addRepositoryLink(String repositoryId, AtomLink link) {
437         addRepositoryLink(repositoryId, link.getRel(), link.getHref());
438     }
439 
440     /**
441      * Gets an URI template from the cache.
442      */
443     protected String getTemplateLink(String repositoryId, String type, Map<String, Object> parameters) {
444         return getLinkCache().getTemplateLink(repositoryId, type, parameters);
445     }
446 
447     /**
448      * Gets a template link from the cache if it is there or loads it into the
449      * cache if it is not there.
450      */
451     protected String loadTemplateLink(String repositoryId, String type, Map<String, Object> parameters) {
452         String link = getTemplateLink(repositoryId, type, parameters);
453         if (link == null) {
454             // cache repository info
455             getRepositoriesInternal(repositoryId);
456             link = getTemplateLink(repositoryId, type, parameters);
457         }
458 
459         return link;
460     }
461 
462     /**
463      * Adds an URI template to the cache.
464      */
465     protected void addTemplate(String repositoryId, String type, String link) {
466         getLinkCache().addTemplate(repositoryId, type, link);
467     }
468 
469     // ---- exceptions ----
470 
471     /**
472      * Converts a HTTP status code into an Exception.
473      */
474     protected CmisBaseException convertStatusCode(int code, String message, String errorContent, Throwable t) {
475         String exception = extractException(errorContent);
476         message = extractErrorMessage(message, errorContent);
477         Map<String, String> additionalData = extractAddtionalData(errorContent);
478 
479         switch (code) {
480         case 301:
481         case 302:
482         case 303:
483         case 307:
484             return new CmisConnectionException("Redirects are not supported (HTTP status code " + code + "): "
485                     + message, errorContent, t);
486         case 400:
487             if (CmisFilterNotValidException.EXCEPTION_NAME.equals(exception)) {
488                 return new CmisFilterNotValidException(message, errorContent, additionalData, t);
489             }
490             return new CmisInvalidArgumentException(message, errorContent, additionalData, t);
491         case 401:
492             return new CmisUnauthorizedException(message, errorContent, additionalData, t);
493         case 403:
494             if (CmisStreamNotSupportedException.EXCEPTION_NAME.equals(exception)) {
495                 return new CmisStreamNotSupportedException(message, errorContent, additionalData, t);
496             }
497             return new CmisPermissionDeniedException(message, errorContent, additionalData, t);
498         case 404:
499             return new CmisObjectNotFoundException(message, errorContent, additionalData, t);
500         case 405:
501             return new CmisNotSupportedException(message, errorContent, additionalData, t);
502         case 407:
503             return new CmisProxyAuthenticationException(message, errorContent, additionalData, t);
504         case 409:
505             if (CmisContentAlreadyExistsException.EXCEPTION_NAME.equals(exception)) {
506                 return new CmisContentAlreadyExistsException(message, errorContent, additionalData, t);
507             } else if (CmisVersioningException.EXCEPTION_NAME.equals(exception)) {
508                 return new CmisVersioningException(message, errorContent, additionalData, t);
509             } else if (CmisUpdateConflictException.EXCEPTION_NAME.equals(exception)) {
510                 return new CmisUpdateConflictException(message, errorContent, additionalData, t);
511             } else if (CmisNameConstraintViolationException.EXCEPTION_NAME.equals(exception)) {
512                 return new CmisNameConstraintViolationException(message, errorContent, additionalData, t);
513             }
514             return new CmisConstraintException(message, errorContent, additionalData, t);
515         case 429:
516             return new CmisTooManyRequestsException(message, errorContent, additionalData, t);
517         case 503:
518             return new CmisServiceUnavailableException(message, errorContent, additionalData, t);
519         default:
520             if (CmisStorageException.EXCEPTION_NAME.equals(exception)) {
521                 return new CmisStorageException(message, errorContent, additionalData, t);
522             }
523             return new CmisRuntimeException(message, errorContent, additionalData, t);
524         }
525     }
526 
527     protected String extractException(String errorContent) {
528         if (errorContent == null) {
529             return null;
530         }
531 
532         int begin = errorContent.indexOf(EXCEPTION_EXCEPTION_BEGIN);
533         int end = errorContent.indexOf(EXCEPTION_EXCEPTION_END);
534 
535         if (begin == -1 || end == -1 || begin > end) {
536             return null;
537         }
538 
539         return errorContent.substring(begin + EXCEPTION_EXCEPTION_BEGIN.length(), end);
540     }
541 
542     protected String extractErrorMessage(String message, String errorContent) {
543         if (errorContent == null) {
544             return message;
545         }
546 
547         int begin = errorContent.indexOf(EXCEPTION_MESSAGE_BEGIN);
548         int end = errorContent.indexOf(EXCEPTION_MESSAGE_END);
549 
550         if (begin == -1 || end == -1 || begin > end) {
551             return message;
552         }
553 
554         return errorContent.substring(begin + EXCEPTION_MESSAGE_BEGIN.length(), end);
555     }
556 
557     protected Map<String, String> extractAddtionalData(String errorContent) {
558         if (errorContent == null) {
559             return null;
560         }
561 
562         Map<String, String> result = null;
563 
564         int pos = 0;
565 
566         while (true) {
567             int keyBegin = errorContent.indexOf(EXCEPTION_KEY_BEGIN, pos);
568             int keyEnd = errorContent.indexOf(EXCEPTION_KEY_END, pos);
569 
570             if (keyBegin == -1 || keyEnd == -1 || keyBegin > keyEnd) {
571                 break;
572             }
573 
574             pos = keyEnd + EXCEPTION_KEY_END.length();
575 
576             int valueBegin = errorContent.indexOf(EXCEPTION_VALUE_BEGIN, pos);
577             int valueEnd = errorContent.indexOf(EXCEPTION_VALUE_END, pos);
578 
579             if (valueBegin == -1 || valueEnd == -1 || valueBegin > valueEnd) {
580                 break;
581             }
582 
583             pos = valueEnd + EXCEPTION_VALUE_END.length();
584 
585             if (result == null) {
586                 result = new HashMap<String, String>();
587             }
588 
589             result.put(errorContent.substring(keyBegin + EXCEPTION_KEY_BEGIN.length(), keyEnd),
590                     errorContent.substring(valueBegin + EXCEPTION_VALUE_BEGIN.length(), valueEnd));
591         }
592 
593         return result;
594     }
595 
596     // ---- helpers ----
597 
598     protected boolean is(String name, AtomElement element) {
599         return name.equals(element.getName().getLocalPart());
600     }
601 
602     protected boolean isStr(String name, AtomElement element) {
603         return is(name, element) && (element.getObject() instanceof String);
604     }
605 
606     protected boolean isInt(String name, AtomElement element) {
607         return is(name, element) && (element.getObject() instanceof BigInteger);
608     }
609 
610     protected boolean isNextLink(AtomElement element) {
611         return Constants.REL_NEXT.equals(((AtomLink) element.getObject()).getRel());
612     }
613 
614     /**
615      * Creates a CMIS object with properties and policy IDs.
616      */
617     protected ObjectDataImpl createObject(Properties properties, String changeToken, List<String> policies) {
618         ObjectDataImpl object = new ObjectDataImpl();
619 
620         boolean omitChangeToken = getSession().get(SessionParameter.OMIT_CHANGE_TOKENS, false);
621 
622         if (properties == null) {
623             properties = new PropertiesImpl();
624             if (changeToken != null && !omitChangeToken) {
625                 ((PropertiesImpl) properties)
626                         .addProperty(new PropertyStringImpl(PropertyIds.CHANGE_TOKEN, changeToken));
627             }
628         } else {
629             if (omitChangeToken) {
630                 if (properties.getProperties().containsKey(PropertyIds.CHANGE_TOKEN)) {
631                     properties = new PropertiesImpl(properties);
632                     ((PropertiesImpl) properties).removeProperty(PropertyIds.CHANGE_TOKEN);
633                 }
634             } else {
635                 if (changeToken != null && !properties.getProperties().containsKey(PropertyIds.CHANGE_TOKEN)) {
636                     properties = new PropertiesImpl(properties);
637                     ((PropertiesImpl) properties).addProperty(new PropertyStringImpl(PropertyIds.CHANGE_TOKEN,
638                             changeToken));
639                 }
640             }
641         }
642 
643         object.setProperties(properties);
644 
645         if (isNotEmpty(policies)) {
646             PolicyIdListImpl policyIdList = new PolicyIdListImpl();
647             policyIdList.setPolicyIds(policies);
648             object.setPolicyIds(policyIdList);
649         }
650 
651         return object;
652     }
653 
654     /**
655      * Creates a CMIS object that only contains an ID in the property list.
656      */
657     protected ObjectData createIdObject(String objectId) {
658         ObjectDataImpl object = new ObjectDataImpl();
659 
660         PropertiesImpl properties = new PropertiesImpl();
661         object.setProperties(properties);
662 
663         properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID, objectId));
664 
665         return object;
666     }
667 
668     /**
669      * Parses an input stream.
670      */
671     @SuppressWarnings("unchecked")
672     protected <T extends AtomBase> T parse(InputStream stream, Class<T> clazz) {
673         AtomPubParser parser = new AtomPubParser(stream);
674 
675         try {
676             parser.parse();
677         } catch (Exception e) {
678             throw new CmisConnectionException("Parsing exception!", e);
679         }
680 
681         AtomBase parseResult = parser.getResults();
682 
683         if (!clazz.isInstance(parseResult)) {
684             throw new CmisConnectionException("Unexpected document! Received: "
685                     + (parseResult == null ? "something unknown" : parseResult.getType()));
686         }
687 
688         return (T) parseResult;
689     }
690 
691     /**
692      * Performs a GET on an URL, checks the response code and returns the
693      * result.
694      */
695     protected Response read(UrlBuilder url) {
696         // make the call
697         Response resp = getHttpInvoker().invokeGET(url, session);
698 
699         // check response code
700         if (resp.getResponseCode() != 200) {
701             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
702         }
703 
704         return resp;
705     }
706 
707     /**
708      * Performs a POST on an URL, checks the response code and returns the
709      * result.
710      */
711     protected Response post(UrlBuilder url, String contentType, Output writer) {
712         // make the call
713         Response resp = getHttpInvoker().invokePOST(url, contentType, writer, session);
714 
715         // check response code
716         if (resp.getResponseCode() != 201) {
717             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
718         }
719 
720         return resp;
721     }
722 
723     /**
724      * Performs a POST on an URL, checks the response code and consumes the
725      * response.
726      */
727     protected void postAndConsume(UrlBuilder url, String contentType, Output writer) {
728         Response resp = post(url, contentType, writer);
729         IOUtils.consumeAndClose(resp.getStream());
730     }
731 
732     /**
733      * Performs a PUT on an URL, checks the response code and returns the
734      * result.
735      */
736     protected Response put(UrlBuilder url, String contentType, Output writer) {
737         return put(url, contentType, null, writer);
738     }
739 
740     /**
741      * Performs a PUT on an URL, checks the response code and returns the
742      * result.
743      */
744     protected Response put(UrlBuilder url, String contentType, Map<String, String> headers, Output writer) {
745         // make the call
746         Response resp = getHttpInvoker().invokePUT(url, contentType, headers, writer, session);
747 
748         // check response code
749         if ((resp.getResponseCode() < 200) || (resp.getResponseCode() > 299)) {
750             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
751         }
752 
753         return resp;
754     }
755 
756     /**
757      * Performs a DELETE on an URL, checks the response code and returns the
758      * result.
759      */
760     protected void delete(UrlBuilder url) {
761         // make the call
762         Response resp = getHttpInvoker().invokeDELETE(url, session);
763 
764         // check response code
765         if (resp.getResponseCode() != 204) {
766             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
767         }
768     }
769 
770     // ---- common operations ----
771 
772     /**
773      * Checks if at least one ACE list is not empty.
774      */
775     protected boolean isAclMergeRequired(Acl addAces, Acl removeAces) {
776         return (addAces != null && isNotEmpty(addAces.getAces()))
777                 || (removeAces != null && isNotEmpty(removeAces.getAces()));
778     }
779 
780     /**
781      * Merges the new ACL from original, add and remove ACEs lists.
782      */
783     protected Acl mergeAcls(Acl originalAces, Acl addAces, Acl removeAces) {
784         Map<String, Set<String>> originals = convertAclToMap(originalAces);
785         Map<String, Set<String>> adds = convertAclToMap(addAces);
786         Map<String, Set<String>> removes = convertAclToMap(removeAces);
787         List<Ace> newAces = new ArrayList<Ace>();
788 
789         // iterate through the original ACEs
790         for (Map.Entry<String, Set<String>> ace : originals.entrySet()) {
791 
792             // add permissions
793             Set<String> addPermissions = adds.get(ace.getKey());
794             if (addPermissions != null) {
795                 ace.getValue().addAll(addPermissions);
796             }
797 
798             // remove permissions
799             Set<String> removePermissions = removes.get(ace.getKey());
800             if (removePermissions != null) {
801                 ace.getValue().removeAll(removePermissions);
802             }
803 
804             // create new ACE
805             if (!ace.getValue().isEmpty()) {
806                 newAces.add(new AccessControlEntryImpl(new AccessControlPrincipalDataImpl(ace.getKey()),
807                         new ArrayList<String>(ace.getValue())));
808             }
809         }
810 
811         // find all ACEs that should be added but are not in the original ACE
812         // list
813         for (Map.Entry<String, Set<String>> ace : adds.entrySet()) {
814             if (!originals.containsKey(ace.getKey()) && !ace.getValue().isEmpty()) {
815                 newAces.add(new AccessControlEntryImpl(new AccessControlPrincipalDataImpl(ace.getKey()),
816                         new ArrayList<String>(ace.getValue())));
817             }
818         }
819 
820         return new AccessControlListImpl(newAces);
821     }
822 
823     /**
824      * Converts a list of ACEs into Map for better handling.
825      */
826     private static Map<String, Set<String>> convertAclToMap(Acl acl) {
827         Map<String, Set<String>> result = new HashMap<String, Set<String>>();
828 
829         if (acl == null || acl.getAces() == null) {
830             return result;
831         }
832 
833         for (Ace ace : acl.getAces()) {
834             // don't consider indirect ACEs - we can't change them
835             if (!ace.isDirect()) {
836                 // ignore
837                 continue;
838             }
839 
840             // although a principal must not be null, check it
841             if (ace.getPrincipal() == null || ace.getPrincipal().getId() == null) {
842                 // ignore
843                 continue;
844             }
845 
846             Set<String> permissions = result.get(ace.getPrincipal().getId());
847             if (permissions == null) {
848                 permissions = new HashSet<String>();
849                 result.put(ace.getPrincipal().getId(), permissions);
850             }
851 
852             if (ace.getPermissions() != null) {
853                 permissions.addAll(ace.getPermissions());
854             }
855         }
856 
857         return result;
858     }
859 
860     /**
861      * Retrieves the Service Document from the server and caches the repository
862      * info objects, collections, links, URI templates, etc.
863      */
864     @SuppressWarnings("unchecked")
865     protected List<RepositoryInfo> getRepositoriesInternal(String repositoryId) {
866         List<RepositoryInfo> repInfos = new ArrayList<RepositoryInfo>();
867 
868         // retrieve service doc
869         UrlBuilder url = new UrlBuilder(getServiceDocURL());
870         url.addParameter(Constants.PARAM_REPOSITORY_ID, repositoryId);
871 
872         // read and parse
873         Response resp = read(url);
874         ServiceDoc serviceDoc = parse(resp.getStream(), ServiceDoc.class);
875 
876         // walk through the workspaces
877         for (RepositoryWorkspace ws : serviceDoc.getWorkspaces()) {
878             if (ws.getId() == null) {
879                 // found a non-CMIS workspace
880                 continue;
881             }
882 
883             for (AtomElement element : ws.getElements()) {
884                 if (is(NAME_COLLECTION, element)) {
885                     Map<String, String> colMap = (Map<String, String>) element.getObject();
886                     addCollection(ws.getId(), colMap.get("collectionType"), colMap.get("href"));
887                 } else if (element.getObject() instanceof AtomLink) {
888                     addRepositoryLink(ws.getId(), (AtomLink) element.getObject());
889                 } else if (is(NAME_URI_TEMPLATE, element)) {
890                     Map<String, String> tempMap = (Map<String, String>) element.getObject();
891                     addTemplate(ws.getId(), tempMap.get("type"), tempMap.get("template"));
892                 } else if (element.getObject() instanceof RepositoryInfo) {
893                     repInfos.add((RepositoryInfo) element.getObject());
894                 }
895             }
896         }
897 
898         return repInfos;
899     }
900 
901     /**
902      * Retrieves an object from the server and caches the links.
903      */
904     protected ObjectData getObjectInternal(String repositoryId, IdentifierType idOrPath, String objectIdOrPath,
905             ReturnVersion returnVersion, String filter, Boolean includeAllowableActions,
906             IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
907             Boolean includeAcl, ExtensionsData extension) {
908 
909         Map<String, Object> parameters = new HashMap<String, Object>();
910         parameters.put(Constants.PARAM_ID, objectIdOrPath);
911         parameters.put(Constants.PARAM_PATH, objectIdOrPath);
912         parameters.put(Constants.PARAM_RETURN_VERSION, returnVersion);
913         parameters.put(Constants.PARAM_FILTER, filter);
914         parameters.put(Constants.PARAM_ALLOWABLE_ACTIONS, includeAllowableActions);
915         parameters.put(Constants.PARAM_ACL, includeAcl);
916         parameters.put(Constants.PARAM_POLICY_IDS, includePolicyIds);
917         parameters.put(Constants.PARAM_RELATIONSHIPS, includeRelationships);
918         parameters.put(Constants.PARAM_RENDITION_FILTER, renditionFilter);
919 
920         String link = loadTemplateLink(repositoryId, (idOrPath == IdentifierType.ID ? Constants.TEMPLATE_OBJECT_BY_ID
921                 : Constants.TEMPLATE_OBJECT_BY_PATH), parameters);
922         if (link == null) {
923             throw new CmisObjectNotFoundException("Unknown repository!");
924         }
925 
926         UrlBuilder url = new UrlBuilder(link);
927         // workaround for missing template parameter in the CMIS spec
928         if (returnVersion != null && returnVersion != ReturnVersion.THIS) {
929             url.addParameter(Constants.PARAM_RETURN_VERSION, returnVersion);
930         }
931 
932         // read and parse
933         Response resp = read(url);
934         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
935 
936         // we expect a CMIS entry
937         if (entry.getId() == null) {
938             throw new CmisConnectionException("Received Atom entry is not a CMIS entry!");
939         }
940 
941         lockLinks();
942         ObjectData result = null;
943         try {
944             // clean up cache
945             removeLinks(repositoryId, entry.getId());
946 
947             // walk through the entry
948             for (AtomElement element : entry.getElements()) {
949                 if (element.getObject() instanceof AtomLink) {
950                     addLink(repositoryId, entry.getId(), (AtomLink) element.getObject());
951                 } else if (element.getObject() instanceof ObjectData) {
952                     result = (ObjectData) element.getObject();
953                 }
954             }
955         } finally {
956             unlockLinks();
957         }
958 
959         return result;
960     }
961 
962     /**
963      * Retrieves a type definition.
964      */
965     protected TypeDefinition getTypeDefinitionInternal(String repositoryId, String typeId) {
966 
967         Map<String, Object> parameters = new HashMap<String, Object>();
968         parameters.put(Constants.PARAM_ID, typeId);
969 
970         String link = loadTemplateLink(repositoryId, Constants.TEMPLATE_TYPE_BY_ID, parameters);
971         if (link == null) {
972             throw new CmisObjectNotFoundException("Unknown repository!");
973         }
974 
975         // read and parse
976         Response resp = read(new UrlBuilder(link));
977         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
978 
979         // we expect a CMIS entry
980         if (entry.getId() == null) {
981             throw new CmisConnectionException("Received Atom entry is not a CMIS entry!");
982         }
983 
984         lockTypeLinks();
985         TypeDefinition result = null;
986         try {
987             // clean up cache
988             removeTypeLinks(repositoryId, entry.getId());
989 
990             // walk through the entry
991             for (AtomElement element : entry.getElements()) {
992                 if (element.getObject() instanceof AtomLink) {
993                     addTypeLink(repositoryId, entry.getId(), (AtomLink) element.getObject());
994                 } else if (element.getObject() instanceof TypeDefinition) {
995                     result = (TypeDefinition) element.getObject();
996                 }
997             }
998         } finally {
999             unlockTypeLinks();
1000         }
1001 
1002         return result;
1003     }
1004 
1005     /**
1006      * Retrieves the ACL of an object.
1007      */
1008     public Acl getAclInternal(String repositoryId, String objectId, Boolean onlyBasicPermissions,
1009             ExtensionsData extension) {
1010 
1011         // find the link
1012         String link = loadLink(repositoryId, objectId, Constants.REL_ACL, Constants.MEDIATYPE_ACL);
1013 
1014         if (link == null) {
1015             throwLinkException(repositoryId, objectId, Constants.REL_ACL, Constants.MEDIATYPE_ACL);
1016         }
1017 
1018         UrlBuilder url = new UrlBuilder(link);
1019         url.addParameter(Constants.PARAM_ONLY_BASIC_PERMISSIONS, onlyBasicPermissions);
1020 
1021         // read and parse
1022         Response resp = read(url);
1023         AtomAcl acl = parse(resp.getStream(), AtomAcl.class);
1024 
1025         return acl.getACL();
1026     }
1027 
1028     /**
1029      * Updates the ACL of an object.
1030      */
1031     protected AtomAcl updateAcl(String repositoryId, String objectId, final Acl acl, AclPropagation aclPropagation) {
1032 
1033         // find the link
1034         String link = loadLink(repositoryId, objectId, Constants.REL_ACL, Constants.MEDIATYPE_ACL);
1035 
1036         if (link == null) {
1037             throwLinkException(repositoryId, objectId, Constants.REL_ACL, Constants.MEDIATYPE_ACL);
1038         }
1039 
1040         UrlBuilder aclUrl = new UrlBuilder(link);
1041         aclUrl.addParameter(Constants.PARAM_ACL_PROPAGATION, aclPropagation);
1042 
1043         final CmisVersion cmisVersion = getCmisVersion(repositoryId);
1044 
1045         // update
1046         Response resp = put(aclUrl, Constants.MEDIATYPE_ACL, new Output() {
1047             @Override
1048             public void write(OutputStream out) throws Exception {
1049                 XMLStreamWriter writer = XMLUtils.createWriter(out);
1050                 XMLUtils.startXmlDocument(writer);
1051                 XMLConverter.writeAcl(writer, cmisVersion, true, acl);
1052                 XMLUtils.endXmlDocument(writer);
1053             }
1054         });
1055 
1056         // parse new entry
1057         return parse(resp.getStream(), AtomAcl.class);
1058     }
1059 
1060 }