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 java.io.IOException;
22  import java.io.InputStream;
23  import java.io.OutputStream;
24  import java.math.BigInteger;
25  import java.util.ArrayList;
26  import java.util.Collections;
27  import java.util.List;
28  import java.util.Map;
29  
30  import javax.xml.stream.XMLStreamException;
31  
32  import org.apache.chemistry.opencmis.client.bindings.spi.BindingSession;
33  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomAllowableActions;
34  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomElement;
35  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomEntry;
36  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomFeed;
37  import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomLink;
38  import org.apache.chemistry.opencmis.client.bindings.spi.http.Output;
39  import org.apache.chemistry.opencmis.client.bindings.spi.http.Response;
40  import org.apache.chemistry.opencmis.commons.PropertyIds;
41  import org.apache.chemistry.opencmis.commons.SessionParameter;
42  import org.apache.chemistry.opencmis.commons.data.Acl;
43  import org.apache.chemistry.opencmis.commons.data.AllowableActions;
44  import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
45  import org.apache.chemistry.opencmis.commons.data.ContentStream;
46  import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
47  import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
48  import org.apache.chemistry.opencmis.commons.data.ObjectData;
49  import org.apache.chemistry.opencmis.commons.data.Properties;
50  import org.apache.chemistry.opencmis.commons.data.PropertyData;
51  import org.apache.chemistry.opencmis.commons.data.PropertyId;
52  import org.apache.chemistry.opencmis.commons.data.PropertyString;
53  import org.apache.chemistry.opencmis.commons.data.RenditionData;
54  import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
55  import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
56  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
57  import org.apache.chemistry.opencmis.commons.exceptions.CmisConnectionException;
58  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
59  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
60  import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
61  import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
62  import org.apache.chemistry.opencmis.commons.impl.Constants;
63  import org.apache.chemistry.opencmis.commons.impl.IOUtils;
64  import org.apache.chemistry.opencmis.commons.impl.MimeHelper;
65  import org.apache.chemistry.opencmis.commons.impl.ReturnVersion;
66  import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
67  import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateImpl;
68  import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
69  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
70  import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
71  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PartialContentStreamImpl;
72  import org.apache.chemistry.opencmis.commons.spi.Holder;
73  import org.apache.chemistry.opencmis.commons.spi.ObjectService;
74  
75  /**
76   * Object Service AtomPub client.
77   */
78  public class ObjectServiceImpl extends AbstractAtomPubService implements ObjectService {
79  
80      /**
81       * Constructor.
82       */
83      public ObjectServiceImpl(BindingSession session) {
84          setSession(session);
85      }
86  
87      @Override
88      public String createDocument(String repositoryId, Properties properties, String folderId,
89              ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
90              Acl removeAces, ExtensionsData extension) {
91          checkCreateProperties(properties);
92  
93          // find the link
94          String link = null;
95  
96          if (folderId == null) {
97              // Creation of unfiled objects via AtomPub is not defined in the
98              // CMIS 1.0 specification. This implementation follow the CMIS 1.1
99              // draft and POSTs the document to the Unfiled collection.
100 
101             link = loadCollection(repositoryId, Constants.COLLECTION_UNFILED);
102 
103             if (link == null) {
104                 throw new CmisObjectNotFoundException("Unknown repository or unfiling not supported!");
105             }
106         } else {
107             link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
108 
109             if (link == null) {
110                 throwLinkException(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
111             }
112         }
113 
114         UrlBuilder url = new UrlBuilder(link);
115         url.addParameter(Constants.PARAM_VERSIONIG_STATE, versioningState);
116 
117         // set up writer
118         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, null, policies),
119                 getCmisVersion(repositoryId), contentStream);
120 
121         // post the new folder object
122         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
123             @Override
124             public void write(OutputStream out) throws XMLStreamException, IOException {
125                 entryWriter.write(out);
126             }
127         });
128 
129         // parse the response
130         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
131 
132         // handle ACL modifications
133         handleAclModifications(repositoryId, entry, addAces, removeAces);
134 
135         return entry.getId();
136     }
137 
138     @Override
139     public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties,
140             String folderId, VersioningState versioningState, List<String> policies, Acl addACEs, Acl removeACEs,
141             ExtensionsData extension) {
142         throw new CmisNotSupportedException("createDocumentFromSource is not supported by the AtomPub binding!");
143     }
144 
145     @Override
146     public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
147             Acl addAces, Acl removeAces, ExtensionsData extension) {
148         checkCreateProperties(properties);
149 
150         // find the link
151         String link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
152 
153         if (link == null) {
154             throwLinkException(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
155         }
156 
157         UrlBuilder url = new UrlBuilder(link);
158 
159         // set up writer
160         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, null, policies),
161                 getCmisVersion(repositoryId));
162 
163         // post the new folder object
164         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
165             @Override
166             public void write(OutputStream out) throws XMLStreamException, IOException {
167                 entryWriter.write(out);
168             }
169         });
170 
171         // parse the response
172         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
173 
174         // handle ACL modifications
175         handleAclModifications(repositoryId, entry, addAces, removeAces);
176 
177         return entry.getId();
178     }
179 
180     @Override
181     public String createPolicy(String repositoryId, Properties properties, String folderId, List<String> policies,
182             Acl addAces, Acl removeAces, ExtensionsData extension) {
183         checkCreateProperties(properties);
184 
185         // find the link
186         String link = null;
187 
188         if (folderId == null) {
189             // Creation of unfiled objects via AtomPub is not defined in the
190             // CMIS 1.0 specification. This implementation follow the CMIS 1.1
191             // draft and POSTs the policy to the Unfiled collection.
192 
193             link = loadCollection(repositoryId, Constants.COLLECTION_UNFILED);
194 
195             if (link == null) {
196                 throw new CmisObjectNotFoundException("Unknown repository or unfiling not supported!");
197             }
198         } else {
199             link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
200 
201             if (link == null) {
202                 throwLinkException(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
203             }
204         }
205 
206         UrlBuilder url = new UrlBuilder(link);
207 
208         // set up writer
209         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, null, policies),
210                 getCmisVersion(repositoryId));
211 
212         // post the new folder object
213         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
214             @Override
215             public void write(OutputStream out) throws XMLStreamException, IOException {
216                 entryWriter.write(out);
217             }
218         });
219 
220         // parse the response
221         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
222 
223         // handle ACL modifications
224         handleAclModifications(repositoryId, entry, addAces, removeAces);
225 
226         return entry.getId();
227     }
228 
229     @Override
230     public String createItem(String repositoryId, Properties properties, String folderId, List<String> policies,
231             Acl addAces, Acl removeAces, ExtensionsData extension) {
232         checkCreateProperties(properties);
233 
234         // find the link
235         String link = null;
236 
237         if (folderId == null) {
238             link = loadCollection(repositoryId, Constants.COLLECTION_UNFILED);
239 
240             if (link == null) {
241                 throw new CmisObjectNotFoundException("Unknown repository or unfiling not supported!");
242             }
243         } else {
244             link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
245 
246             if (link == null) {
247                 throwLinkException(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
248             }
249         }
250 
251         UrlBuilder url = new UrlBuilder(link);
252 
253         // set up writer
254         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, null, policies),
255                 getCmisVersion(repositoryId));
256 
257         // post the new folder object
258         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
259             @Override
260             public void write(OutputStream out) throws XMLStreamException, IOException {
261                 entryWriter.write(out);
262             }
263         });
264 
265         // parse the response
266         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
267 
268         // handle ACL modifications
269         handleAclModifications(repositoryId, entry, addAces, removeAces);
270 
271         return entry.getId();
272     }
273 
274     @Override
275     public String createRelationship(String repositoryId, Properties properties, List<String> policies, Acl addAces,
276             Acl removeAces, ExtensionsData extension) {
277         checkCreateProperties(properties);
278 
279         // find source id
280         PropertyData<?> sourceIdProperty = properties.getProperties().get(PropertyIds.SOURCE_ID);
281         if (!(sourceIdProperty instanceof PropertyId)) {
282             throw new CmisInvalidArgumentException("Source Id is not set!");
283         }
284 
285         String sourceId = ((PropertyId) sourceIdProperty).getFirstValue();
286         if (sourceId == null) {
287             throw new CmisInvalidArgumentException("Source Id is not set!");
288         }
289 
290         // find the link
291         String link = loadLink(repositoryId, sourceId, Constants.REL_RELATIONSHIPS, Constants.MEDIATYPE_FEED);
292 
293         if (link == null) {
294             throwLinkException(repositoryId, sourceId, Constants.REL_RELATIONSHIPS, Constants.MEDIATYPE_FEED);
295         }
296 
297         UrlBuilder url = new UrlBuilder(link);
298 
299         // set up writer
300         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, null, policies),
301                 getCmisVersion(repositoryId));
302 
303         // post the new folder object
304         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
305             @Override
306             public void write(OutputStream out) throws XMLStreamException, IOException {
307                 entryWriter.write(out);
308             }
309         });
310 
311         // parse the response
312         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
313 
314         // handle ACL modifications
315         handleAclModifications(repositoryId, entry, addAces, removeAces);
316 
317         return entry.getId();
318     }
319 
320     @Override
321     public void updateProperties(String repositoryId, Holder<String> objectId, Holder<String> changeToken,
322             Properties properties, ExtensionsData extension) {
323         // we need an object id
324         if ((objectId == null) || (objectId.getValue() == null) || (objectId.getValue().length() == 0)) {
325             throw new CmisInvalidArgumentException("Object ID must be set!");
326         }
327 
328         // find the link
329         String link = loadLink(repositoryId, objectId.getValue(), Constants.REL_SELF, Constants.MEDIATYPE_ENTRY);
330 
331         if (link == null) {
332             throwLinkException(repositoryId, objectId.getValue(), Constants.REL_SELF, Constants.MEDIATYPE_ENTRY);
333         }
334 
335         UrlBuilder url = new UrlBuilder(link);
336         if (changeToken != null) {
337             if (getSession().get(SessionParameter.OMIT_CHANGE_TOKENS, false)) {
338                 changeToken.setValue(null);
339             } else {
340                 // not required by the CMIS specification
341                 // -> keep for backwards compatibility with older OpenCMIS
342                 // servers
343                 url.addParameter(Constants.PARAM_CHANGE_TOKEN, changeToken.getValue());
344             }
345         }
346 
347         // set up writer
348         final AtomEntryWriter entryWriter = new AtomEntryWriter(createObject(properties, changeToken == null ? null
349                 : changeToken.getValue(), null), getCmisVersion(repositoryId));
350 
351         // update
352         Response resp = put(url, Constants.MEDIATYPE_ENTRY, new Output() {
353             @Override
354             public void write(OutputStream out) throws XMLStreamException, IOException {
355                 entryWriter.write(out);
356             }
357         });
358 
359         // parse new entry
360         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
361 
362         // we expect a CMIS entry
363         if (entry.getId() == null) {
364             throw new CmisConnectionException("Received Atom entry is not a CMIS entry!");
365         }
366 
367         // set object id
368         objectId.setValue(entry.getId());
369 
370         if (changeToken != null) {
371             changeToken.setValue(null); // just in case
372         }
373 
374         lockLinks();
375         try {
376             // clean up cache
377             removeLinks(repositoryId, entry.getId());
378 
379             // walk through the entry
380             for (AtomElement element : entry.getElements()) {
381                 if (element.getObject() instanceof AtomLink) {
382                     addLink(repositoryId, entry.getId(), (AtomLink) element.getObject());
383                 } else if (element.getObject() instanceof ObjectData) {
384                     // extract new change token
385                     if (changeToken != null) {
386                         ObjectData object = (ObjectData) element.getObject();
387 
388                         if (object.getProperties() != null) {
389                             Object changeTokenStr = object.getProperties().getProperties()
390                                     .get(PropertyIds.CHANGE_TOKEN);
391                             if (changeTokenStr instanceof PropertyString) {
392                                 changeToken.setValue(((PropertyString) changeTokenStr).getFirstValue());
393                             }
394                         }
395                     }
396                 }
397             }
398         } finally {
399             unlockLinks();
400         }
401     }
402 
403     @Override
404     public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(String repositoryId,
405             List<BulkUpdateObjectIdAndChangeToken> objectIdAndChangeToken, Properties properties,
406             List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds, ExtensionsData extension) {
407         // find link
408         String link = loadCollection(repositoryId, Constants.COLLECTION_BULK_UPDATE);
409 
410         if (link == null) {
411             throw new CmisObjectNotFoundException("Unknown repository or bulk update properties is not supported!");
412         }
413 
414         // set up writer
415         final BulkUpdateImpl bulkUpdate = new BulkUpdateImpl();
416         bulkUpdate.setObjectIdAndChangeToken(objectIdAndChangeToken);
417         bulkUpdate.setProperties(properties);
418         bulkUpdate.setAddSecondaryTypeIds(addSecondaryTypeIds);
419         bulkUpdate.setRemoveSecondaryTypeIds(removeSecondaryTypeIds);
420 
421         final AtomEntryWriter entryWriter = new AtomEntryWriter(bulkUpdate);
422 
423         // post the new folder object
424         Response resp = post(new UrlBuilder(link), Constants.MEDIATYPE_ENTRY, new Output() {
425             @Override
426             public void write(OutputStream out) throws XMLStreamException, IOException {
427                 entryWriter.write(out);
428             }
429         });
430 
431         AtomFeed feed = parse(resp.getStream(), AtomFeed.class);
432         List<BulkUpdateObjectIdAndChangeToken> result = new ArrayList<BulkUpdateObjectIdAndChangeToken>(feed
433                 .getEntries().size());
434 
435         // get the results
436         if (!feed.getEntries().isEmpty()) {
437 
438             for (AtomEntry entry : feed.getEntries()) {
439                 // walk through the entry
440                 // we are not interested in the links this time because they
441                 // could belong to a new document version
442                 for (AtomElement element : entry.getElements()) {
443                     if (element.getObject() instanceof ObjectData) {
444                         ObjectData object = (ObjectData) element.getObject();
445                         String id = object.getId();
446                         if (id != null) {
447                             String changeToken = null;
448                             PropertyData<?> changeTokenProp = object.getProperties().getProperties()
449                                     .get(PropertyIds.CHANGE_TOKEN);
450                             if (changeTokenProp instanceof PropertyString) {
451                                 changeToken = ((PropertyString) changeTokenProp).getFirstValue();
452                             }
453 
454                             result.add(new BulkUpdateObjectIdAndChangeTokenImpl(id, changeToken));
455                         }
456                     }
457                 }
458             }
459         }
460 
461         return result;
462     }
463 
464     @Override
465     public void deleteObject(String repositoryId, String objectId, Boolean allVersions, ExtensionsData extension) {
466 
467         // find the link
468         String link = loadLink(repositoryId, objectId, Constants.REL_SELF, Constants.MEDIATYPE_ENTRY);
469 
470         if (link == null) {
471             throwLinkException(repositoryId, objectId, Constants.REL_SELF, Constants.MEDIATYPE_ENTRY);
472         }
473 
474         UrlBuilder url = new UrlBuilder(link);
475         url.addParameter(Constants.PARAM_ALL_VERSIONS, allVersions);
476 
477         delete(url);
478     }
479 
480     @Override
481     public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
482             UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
483 
484         // find the down links
485         String link = loadLink(repositoryId, folderId, Constants.REL_DOWN, null);
486         String childrenLink = null;
487 
488         if (link != null) {
489             // found only a children link, but no descendants link
490             // -> try folder tree link
491             childrenLink = link;
492             link = null;
493         } else {
494             // found no or two down links
495             // -> get only the descendants link
496             link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_DESCENDANTS);
497         }
498 
499         if (link == null) {
500             link = loadLink(repositoryId, folderId, Constants.REL_FOLDERTREE, Constants.MEDIATYPE_DESCENDANTS);
501         }
502 
503         if (link == null) {
504             link = loadLink(repositoryId, folderId, Constants.REL_FOLDERTREE, Constants.MEDIATYPE_FEED);
505         }
506 
507         if (link == null) {
508             link = childrenLink;
509         }
510 
511         if (link == null) {
512             throwLinkException(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_DESCENDANTS);
513         }
514 
515         UrlBuilder url = new UrlBuilder(link);
516         url.addParameter(Constants.PARAM_ALL_VERSIONS, allVersions);
517         url.addParameter(Constants.PARAM_UNFILE_OBJECTS, unfileObjects);
518         url.addParameter(Constants.PARAM_CONTINUE_ON_FAILURE, continueOnFailure);
519 
520         // make the call
521         Response resp = getHttpInvoker().invokeDELETE(url, getSession());
522 
523         // check response code
524         if (resp.getResponseCode() == 200 || resp.getResponseCode() == 202 || resp.getResponseCode() == 204) {
525             return new FailedToDeleteDataImpl();
526         }
527 
528         // If the server returned an internal server error, get the remaining
529         // children of the folder. We only retrieve the first level, since
530         // getDescendants() is not supported by all repositories.
531         if (resp.getResponseCode() == 500) {
532             link = loadLink(repositoryId, folderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
533 
534             if (link != null) {
535                 url = new UrlBuilder(link);
536                 // we only want the object ids
537                 url.addParameter(Constants.PARAM_FILTER, "cmis:objectId");
538                 url.addParameter(Constants.PARAM_ALLOWABLE_ACTIONS, false);
539                 url.addParameter(Constants.PARAM_RELATIONSHIPS, IncludeRelationships.NONE);
540                 url.addParameter(Constants.PARAM_RENDITION_FILTER, "cmis:none");
541                 url.addParameter(Constants.PARAM_PATH_SEGMENT, false);
542                 // 1000 children should be enough to indicate a problem
543                 url.addParameter(Constants.PARAM_MAX_ITEMS, 1000);
544                 url.addParameter(Constants.PARAM_SKIP_COUNT, 0);
545 
546                 // read and parse
547                 resp = read(url);
548                 AtomFeed feed = parse(resp.getStream(), AtomFeed.class);
549 
550                 // prepare result
551                 FailedToDeleteDataImpl result = new FailedToDeleteDataImpl();
552                 List<String> ids = new ArrayList<String>();
553                 result.setIds(ids);
554 
555                 // get the children ids
556                 for (AtomEntry entry : feed.getEntries()) {
557                     ids.add(entry.getId());
558                 }
559 
560                 return result;
561             }
562         }
563 
564         throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
565     }
566 
567     @Override
568     public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
569         // find the link
570         String link = loadLink(repositoryId, objectId, Constants.REL_ALLOWABLEACTIONS,
571                 Constants.MEDIATYPE_ALLOWABLEACTION);
572 
573         if (link == null) {
574             throwLinkException(repositoryId, objectId, Constants.REL_ALLOWABLEACTIONS,
575                     Constants.MEDIATYPE_ALLOWABLEACTION);
576         }
577 
578         UrlBuilder url = new UrlBuilder(link);
579 
580         // read and parse
581         Response resp = read(url);
582         AtomAllowableActions allowableActions = parse(resp.getStream(), AtomAllowableActions.class);
583 
584         return allowableActions.getAllowableActions();
585     }
586 
587     @Override
588     public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
589             BigInteger length, ExtensionsData extension) {
590         // find the link
591         String link = null;
592         if (streamId != null) {
593             // use the alternate link per spec
594             link = loadLink(repositoryId, objectId, Constants.REL_ALTERNATE, streamId);
595             if (link != null) {
596                 streamId = null; // we have a full URL now
597             }
598         }
599         if (link == null) {
600             link = loadLink(repositoryId, objectId, AtomPubParser.LINK_REL_CONTENT, null);
601         }
602 
603         if (link == null) {
604             throw new CmisConstraintException("No content stream");
605         }
606 
607         UrlBuilder url = new UrlBuilder(link);
608         // using the content URL and adding a streamId param
609         // is not spec-compliant
610         url.addParameter(Constants.PARAM_STREAM_ID, streamId);
611 
612         // get the content
613         Response resp = getHttpInvoker().invokeGET(url, getSession(), offset, length);
614 
615         // check response code
616         if ((resp.getResponseCode() != 200) && (resp.getResponseCode() != 206)) {
617             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
618         }
619 
620         ContentStreamImpl result;
621         if (resp.getResponseCode() == 206) {
622             result = new PartialContentStreamImpl();
623         } else {
624             result = new ContentStreamImpl();
625         }
626 
627         String filename = null;
628         String contentDisposition = resp.getHeader("Content-Disposition");
629         if (contentDisposition != null) {
630             filename = MimeHelper.decodeContentDispositionFilename(contentDisposition);
631         }
632 
633         result.setFileName(filename);
634         result.setLength(resp.getContentLength());
635         result.setMimeType(resp.getContentTypeHeader());
636         result.setStream(resp.getStream());
637 
638         return result;
639     }
640 
641     @Override
642     public ObjectData getObject(String repositoryId, String objectId, String filter, Boolean includeAllowableActions,
643             IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
644             Boolean includeACL, ExtensionsData extension) {
645 
646         return getObjectInternal(repositoryId, IdentifierType.ID, objectId, ReturnVersion.THIS, filter,
647                 includeAllowableActions, includeRelationships, renditionFilter, includePolicyIds, includeACL, extension);
648     }
649 
650     @Override
651     public ObjectData getObjectByPath(String repositoryId, String path, String filter, Boolean includeAllowableActions,
652             IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
653             Boolean includeACL, ExtensionsData extension) {
654 
655         return getObjectInternal(repositoryId, IdentifierType.PATH, path, ReturnVersion.THIS, filter,
656                 includeAllowableActions, includeRelationships, renditionFilter, includePolicyIds, includeACL, extension);
657     }
658 
659     @Override
660     public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
661         ObjectData object = getObjectInternal(repositoryId, IdentifierType.ID, objectId, ReturnVersion.THIS, filter,
662                 Boolean.FALSE, IncludeRelationships.NONE, "cmis:none", Boolean.FALSE, Boolean.FALSE, extension);
663 
664         return object.getProperties();
665     }
666 
667     @Override
668     public List<RenditionData> getRenditions(String repositoryId, String objectId, String renditionFilter,
669             BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
670         ObjectData object = getObjectInternal(repositoryId, IdentifierType.ID, objectId, ReturnVersion.THIS,
671                 PropertyIds.OBJECT_ID, Boolean.FALSE, IncludeRelationships.NONE, renditionFilter, Boolean.FALSE,
672                 Boolean.FALSE, extension);
673 
674         List<RenditionData> result = object.getRenditions();
675         if (result == null) {
676             result = Collections.emptyList();
677         }
678 
679         return result;
680     }
681 
682     @Override
683     public void moveObject(String repositoryId, Holder<String> objectId, String targetFolderId, String sourceFolderId,
684             ExtensionsData extension) {
685         if (objectId == null || objectId.getValue() == null || objectId.getValue().length() == 0) {
686             throw new CmisInvalidArgumentException("Object ID must be set!");
687         }
688 
689         if (targetFolderId == null || targetFolderId.length() == 0 || sourceFolderId == null
690                 || sourceFolderId.length() == 0) {
691             throw new CmisInvalidArgumentException("Source and target folder must be set!");
692         }
693 
694         // find the link
695         String link = loadLink(repositoryId, targetFolderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
696 
697         if (link == null) {
698             throwLinkException(repositoryId, targetFolderId, Constants.REL_DOWN, Constants.MEDIATYPE_CHILDREN);
699         }
700 
701         UrlBuilder url = new UrlBuilder(link);
702         url.addParameter(Constants.PARAM_SOURCE_FOLDER_ID, sourceFolderId);
703 
704         // workaround for SharePoint 2010 - see CMIS-839
705         boolean objectIdOnMove = getSession().get(SessionParameter.INCLUDE_OBJECTID_URL_PARAM_ON_MOVE, false);
706         if (objectIdOnMove) {
707             url.addParameter("objectId", objectId.getValue());
708             url.addParameter("targetFolderId", targetFolderId);
709         }
710 
711         // set up object and writer
712         final AtomEntryWriter entryWriter = new AtomEntryWriter(createIdObject(objectId.getValue()),
713                 getCmisVersion(repositoryId));
714 
715         // post move request
716         Response resp = post(url, Constants.MEDIATYPE_ENTRY, new Output() {
717             @Override
718             public void write(OutputStream out) throws XMLStreamException, IOException {
719                 entryWriter.write(out);
720             }
721         });
722 
723         // workaround for SharePoint 2010 - see CMIS-839
724         if (objectIdOnMove) {
725             // SharePoint doesn't return a new object ID
726             // we assume that the object ID hasn't changed
727             return;
728         }
729 
730         // parse the response
731         AtomEntry entry = parse(resp.getStream(), AtomEntry.class);
732 
733         objectId.setValue(entry.getId());
734     }
735 
736     @Override
737     public void setContentStream(String repositoryId, Holder<String> objectId, Boolean overwriteFlag,
738             Holder<String> changeToken, ContentStream contentStream, ExtensionsData extension) {
739         setOrAppendContent(repositoryId, objectId, overwriteFlag, changeToken, contentStream, true, false, extension);
740     }
741 
742     @Override
743     public void deleteContentStream(String repositoryId, Holder<String> objectId, Holder<String> changeToken,
744             ExtensionsData extension) {
745         // we need an object id
746         if ((objectId == null) || (objectId.getValue() == null)) {
747             throw new CmisInvalidArgumentException("Object ID must be set!");
748         }
749 
750         // find the link
751         String link = loadLink(repositoryId, objectId.getValue(), Constants.REL_EDITMEDIA, null);
752 
753         if (link == null) {
754             throwLinkException(repositoryId, objectId.getValue(), Constants.REL_EDITMEDIA, null);
755         }
756 
757         UrlBuilder url = new UrlBuilder(link);
758         if (changeToken != null && !getSession().get(SessionParameter.OMIT_CHANGE_TOKENS, false)) {
759             url.addParameter(Constants.PARAM_CHANGE_TOKEN, changeToken.getValue());
760         }
761 
762         delete(url);
763 
764         objectId.setValue(null);
765         if (changeToken != null) {
766             changeToken.setValue(null);
767         }
768     }
769 
770     @Override
771     public void appendContentStream(String repositoryId, Holder<String> objectId, Holder<String> changeToken,
772             ContentStream contentStream, boolean isLastChunk, ExtensionsData extension) {
773         setOrAppendContent(repositoryId, objectId, null, changeToken, contentStream, isLastChunk, true, extension);
774     }
775 
776     // ---- internal ----
777 
778     private static void checkCreateProperties(Properties properties) {
779         if ((properties == null) || (properties.getProperties() == null)) {
780             throw new CmisInvalidArgumentException("Properties must be set!");
781         }
782 
783         if (!properties.getProperties().containsKey(PropertyIds.OBJECT_TYPE_ID)) {
784             throw new CmisInvalidArgumentException("Property " + PropertyIds.OBJECT_TYPE_ID + " must be set!");
785         }
786 
787         if (properties.getProperties().containsKey(PropertyIds.OBJECT_ID)) {
788             throw new CmisInvalidArgumentException("Property " + PropertyIds.OBJECT_ID + " must not be set!");
789         }
790     }
791 
792     /**
793      * Handles ACL modifications of newly created objects.
794      */
795     private void handleAclModifications(String repositoryId, AtomEntry entry, Acl addAces, Acl removeAces) {
796         if (!isAclMergeRequired(addAces, removeAces)) {
797             return;
798         }
799 
800         Acl originalAces = getAclInternal(repositoryId, entry.getId(), Boolean.FALSE, null);
801 
802         if (originalAces != null) {
803             // merge and update ACL
804             Acl newACL = mergeAcls(originalAces, addAces, removeAces);
805             if (newACL != null) {
806                 updateAcl(repositoryId, entry.getId(), newACL, null);
807             }
808         }
809     }
810 
811     /**
812      * Sets or appends content.
813      */
814     private void setOrAppendContent(String repositoryId, Holder<String> objectId, Boolean overwriteFlag,
815             Holder<String> changeToken, ContentStream contentStream, boolean isLastChunk, boolean append,
816             ExtensionsData extension) {
817         // we need an object id
818         if ((objectId == null) || (objectId.getValue() == null)) {
819             throw new CmisInvalidArgumentException("Object ID must be set!");
820         }
821 
822         // we need content
823         if ((contentStream == null) || (contentStream.getStream() == null) || (contentStream.getMimeType() == null)) {
824             throw new CmisInvalidArgumentException("Content must be set!");
825         }
826 
827         // find the link
828         String link = loadLink(repositoryId, objectId.getValue(), Constants.REL_EDITMEDIA, null);
829 
830         if (link == null) {
831             throwLinkException(repositoryId, objectId.getValue(), Constants.REL_EDITMEDIA, null);
832         }
833 
834         UrlBuilder url = new UrlBuilder(link);
835         if (changeToken != null && !getSession().get(SessionParameter.OMIT_CHANGE_TOKENS, false)) {
836             url.addParameter(Constants.PARAM_CHANGE_TOKEN, changeToken.getValue());
837         }
838 
839         if (append) {
840             url.addParameter(Constants.PARAM_APPEND, Boolean.TRUE);
841             url.addParameter(Constants.PARAM_IS_LAST_CHUNK, isLastChunk);
842         } else {
843             url.addParameter(Constants.PARAM_OVERWRITE_FLAG, overwriteFlag);
844         }
845 
846         final InputStream stream = contentStream.getStream();
847 
848         // Content-Disposition header for the filename
849         Map<String, String> headers = null;
850         if (contentStream.getFileName() != null) {
851             headers = Collections
852                     .singletonMap(
853                             MimeHelper.CONTENT_DISPOSITION,
854                             MimeHelper.encodeContentDisposition(MimeHelper.DISPOSITION_ATTACHMENT,
855                                     contentStream.getFileName()));
856         }
857 
858         // send content
859         Response resp = put(url, contentStream.getMimeType(), headers, new Output() {
860             @Override
861             public void write(OutputStream out) throws IOException {
862                 IOUtils.copy(stream, out);
863             }
864         });
865 
866         // check response code further
867         if ((resp.getResponseCode() != 200) && (resp.getResponseCode() != 201) && (resp.getResponseCode() != 204)) {
868             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
869         }
870 
871         if (resp.getResponseCode() == 201) {
872             // unset the object ID if a new resource has been created
873             // (if the resource has been updated (200 and 204), the object ID
874             // hasn't changed)
875             objectId.setValue(null);
876         }
877 
878         if (changeToken != null) {
879             changeToken.setValue(null);
880         }
881     }
882 }