001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.client.impl;
017
018import static org.apache.http.HttpStatus.SC_CONFLICT;
019import static org.apache.http.HttpStatus.SC_FORBIDDEN;
020import static org.apache.http.HttpStatus.SC_NO_CONTENT;
021import static org.apache.http.HttpStatus.SC_NOT_FOUND;
022import static org.slf4j.LoggerFactory.getLogger;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.text.ParseException;
027import java.text.SimpleDateFormat;
028import java.util.Collection;
029import java.util.Date;
030import java.util.HashSet;
031import java.util.Iterator;
032import java.util.Set;
033
034import org.apache.http.HttpResponse;
035import org.apache.http.HttpStatus;
036import org.apache.http.StatusLine;
037import org.apache.http.client.methods.HttpDelete;
038import org.apache.http.client.methods.HttpPatch;
039import org.apache.http.client.methods.HttpPost;
040import org.apache.http.client.methods.HttpPut;
041import org.fcrepo.client.FedoraException;
042import org.fcrepo.client.FedoraRepository;
043import org.fcrepo.client.FedoraResource;
044import org.fcrepo.client.ForbiddenException;
045import org.fcrepo.client.NotFoundException;
046import org.fcrepo.client.utils.HttpCopy;
047import org.fcrepo.client.utils.HttpHelper;
048import org.fcrepo.client.utils.HttpMove;
049import org.fcrepo.kernel.api.RdfLexicon;
050import org.slf4j.Logger;
051
052import com.hp.hpl.jena.graph.Graph;
053import com.hp.hpl.jena.graph.Node;
054import com.hp.hpl.jena.graph.NodeFactory;
055import com.hp.hpl.jena.graph.Triple;
056import com.hp.hpl.jena.rdf.model.Property;
057import com.hp.hpl.jena.util.iterator.ExtendedIterator;
058
059/**
060 * A Fedora Object Impl.
061 *
062 * @author lsitu
063 * @author escowles
064 * @since 2014-08-11
065 */
066public class FedoraResourceImpl implements FedoraResource {
067    private static final Logger LOGGER = getLogger(FedoraResourceImpl.class);
068
069    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
070
071    protected FedoraRepository repository = null;
072
073    protected HttpHelper httpHelper = null;
074
075    protected String path = null;
076
077    protected String oldPath = null;
078
079    protected Node subject = null;
080
081    protected Graph graph;
082
083    private String etagValue = null;
084
085    /**
086     * FedoraResourceImpl constructor
087     *
088     * @param repository FedoraRepositoryImpl that created this resource
089     * @param httpHelper HTTP helper for making repository requests
090     * @param path Repository path of this resource
091     */
092    public FedoraResourceImpl(final FedoraRepository repository, final HttpHelper httpHelper, final String path) {
093        this.repository = repository;
094        this.httpHelper = httpHelper;
095        this.path = path;
096        subject = NodeFactory.createURI(repository.getRepositoryUrl() + path);
097    }
098
099    @Override
100    public void copy(final String destination) throws FedoraException {
101
102        final HttpCopy copy = httpHelper.createCopyMethod(path,destination);
103
104        try {
105            final HttpResponse response = httpHelper.execute( copy );
106            final StatusLine status = response.getStatusLine();
107            final String uri = copy.getURI().toString();
108
109            if (status.getStatusCode() == HttpStatus.SC_CREATED) { // Created
110                 LOGGER.debug("resource successfully copied from " + path + " to " + destination, uri);
111            } else if (status.getStatusCode() == HttpStatus.SC_CONFLICT) { // Source path doesn't exists
112                LOGGER.error("error copying resource {}: {} {}", uri, status.getStatusCode(),
113                        status.getReasonPhrase());
114                throw new FedoraException("error copying resource " + uri + ": " + status.getStatusCode() + " " +
115                                     status.getReasonPhrase());
116            } else if (status.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED) { // Destination path already exists
117                LOGGER.error("error copying resource {}: {} {}", uri, status.getStatusCode(),
118                        status.getReasonPhrase());
119                throw new FedoraException("error copying resource " + uri + ": " + status.getStatusCode() + " " +
120                                     status.getReasonPhrase());
121            } else if (status.getStatusCode() == HttpStatus.SC_BAD_GATEWAY) {
122                // Destination URI isn't a valid resource path
123                LOGGER.error("error copying resource {}: {} {}", uri, status.getStatusCode(),
124                        status.getReasonPhrase());
125                throw new FedoraException("error copying resource " + uri + ": " + status.getStatusCode() + " " +
126                                     status.getReasonPhrase());
127            }
128        } catch (final FedoraException e) {
129            throw e;
130        } catch (final Exception e) {
131            LOGGER.error("could not encode URI parameter", e);
132            throw new FedoraException(e);
133        } finally {
134            copy.releaseConnection();
135        }
136    }
137
138    @Override
139    public void delete() throws FedoraException {
140        final HttpDelete delete = httpHelper.createDeleteMethod(path);
141
142        try {
143            final HttpResponse response = httpHelper.execute( delete );
144            final StatusLine status = response.getStatusLine();
145            final String uri = delete.getURI().toString();
146
147            if ( status.getStatusCode() == SC_NO_CONTENT) {
148                LOGGER.debug("triples updated successfully for resource {}", uri);
149            } else if ( status.getStatusCode() == SC_NOT_FOUND) {
150                LOGGER.error("resource {} does not exist, cannot update", uri);
151                throw new NotFoundException("resource " + uri + " does not exist, cannot update");
152            } else {
153                LOGGER.error("error updating resource {}: {} {}", uri, status.getStatusCode(),
154                             status.getReasonPhrase());
155                throw new FedoraException("error updating resource " + uri + ": " + status.getStatusCode() + " " +
156                                          status.getReasonPhrase());
157            }
158        } catch (final FedoraException e) {
159            throw e;
160        } catch (final Exception e) {
161            LOGGER.error("Error executing request", e);
162            throw new FedoraException(e);
163        } finally {
164            delete.releaseConnection();
165        }
166    }
167
168    @Override
169    public void forceDelete() throws FedoraException {
170            delete();
171            removeTombstone();
172    }
173
174    /**
175     * Remove tombstone (for the current path)
176     */
177    public void removeTombstone() throws FedoraException {
178        removeTombstone(path);
179    }
180
181
182    /**
183     * Remove tombstone located at given path
184     */
185    public void removeTombstone(final String path) throws FedoraException {
186        final HttpDelete delete = httpHelper.createDeleteMethod(path + "/fcr:tombstone");
187
188        try {
189            final HttpResponse response = httpHelper.execute( delete );
190            final StatusLine status = response.getStatusLine();
191            final String uri = delete.getURI().toString();
192
193            if ( status.getStatusCode() == SC_NO_CONTENT) {
194                LOGGER.debug("triples updated successfully for resource {}", uri);
195            } else if ( status.getStatusCode() == SC_NOT_FOUND) {
196                LOGGER.error("resource {} does not exist, cannot update", uri);
197                throw new NotFoundException("resource " + uri + " does not exist, cannot update");
198            } else {
199                LOGGER.error("error updating resource {}: {} {}", uri, status.getStatusCode(),
200                             status.getReasonPhrase());
201                throw new FedoraException("error updating resource " + uri + ": " + status.getStatusCode() + " " +
202                                          status.getReasonPhrase());
203            }
204        } catch (final FedoraException e) {
205            throw e;
206        } catch (final Exception e) {
207            LOGGER.error("Error executing request", e);
208            throw new FedoraException(e);
209        } finally {
210            delete.releaseConnection();
211        }
212    }
213
214    @Override
215    public Date getCreatedDate() {
216        return getDate(RdfLexicon.CREATED_DATE);
217    }
218
219    @Override
220    public String getEtagValue() {
221        return etagValue;
222    }
223
224    /**
225     * set etagValue
226     *
227     * @param etagValue string of etagvalue to set
228     */
229    public void setEtagValue(final String etagValue) {
230        this.etagValue = etagValue;
231    }
232
233    @Override
234    public Date getLastModifiedDate() {
235        return getDate(RdfLexicon.LAST_MODIFIED_DATE);
236    }
237
238    @Override
239    public Collection<String> getMixins() {
240        return getPropertyValues(RdfLexicon.HAS_MIXIN_TYPE);
241    }
242
243    @Override
244    public String getName() {
245        final String p = path.endsWith("/") ? path.substring(0, path.length() - 1) : path;
246        final String[] paths = p.split("/");
247        return paths[paths.length - 1];
248    }
249
250    @Override
251    public String getPath() {
252        return path;
253    }
254
255    @Override
256    public Iterator<Triple> getProperties() {
257        return graph.find(Node.ANY, Node.ANY, Node.ANY);
258    }
259
260    @Override
261    public Long getSize() {
262        return (long) graph.size();
263    }
264
265    @Override
266    public void move(final String destination) throws FedoraException {
267        final HttpMove move = httpHelper.createMoveMethod(path,destination);
268
269        try {
270            final HttpResponse response = httpHelper.execute( move );
271            final StatusLine status = response.getStatusLine();
272            final String uri = move.getURI().toString();
273
274            if (status.getStatusCode() == HttpStatus.SC_CREATED) { // Created
275                LOGGER.debug("resource successfully moved from " + path + " to " + destination, uri);
276                oldPath = path;
277                path = destination;
278                subject = NodeFactory.createURI(repository.getRepositoryUrl() + path);
279            } else if (status.getStatusCode() == HttpStatus.SC_CONFLICT) { // Source path doesn't exists
280                LOGGER.error("error moving resource {}: {} {}", uri, status.getStatusCode(),
281                        status.getReasonPhrase());
282                throw new FedoraException("error moving resource " + uri + ": " + status.getStatusCode() +
283                        " " + status.getReasonPhrase());
284            } else if (status.getStatusCode() == HttpStatus.SC_PRECONDITION_FAILED) {
285                // Destination path already exists
286                LOGGER.error("error moving resource {}: {} {}", uri, status.getStatusCode(),
287                        status.getReasonPhrase());
288                throw new FedoraException("error moving resource " + uri + ": " + status.getStatusCode() + " " +
289                                     status.getReasonPhrase());
290            } else if (status.getStatusCode() == HttpStatus.SC_BAD_GATEWAY) {
291                // Destination URI isn't a valid resource path
292                LOGGER.error("error moving resource {}: {} {}", uri, status.getStatusCode(),
293                        status.getReasonPhrase());
294                throw new FedoraException("error moving resource " + uri + ": " + status.getStatusCode() + " " +
295                                     status.getReasonPhrase());
296            }
297        } catch (final FedoraException e) {
298            oldPath = null;
299            throw e;
300        } catch (final Exception e) {
301            LOGGER.error("could not encode URI parameter", e);
302            throw new FedoraException(e);
303        } finally {
304            move.releaseConnection();
305        }
306    }
307
308    @Override
309    public void forceMove(final String destination) throws FedoraException {
310        move(destination);
311        removeTombstone(oldPath);
312    }
313
314    @Override
315    public void updateProperties(final String sparqlUpdate) throws FedoraException {
316        final HttpPatch patch = httpHelper.createPatchMethod(getPropertiesPath(), sparqlUpdate);
317
318        try {
319            final HttpResponse response = httpHelper.execute( patch );
320            final StatusLine status = response.getStatusLine();
321            final String uri = patch.getURI().toString();
322
323            if ( status.getStatusCode() == SC_NO_CONTENT) {
324                LOGGER.debug("triples updated successfully for resource {}", uri);
325            } else if ( status.getStatusCode() == SC_FORBIDDEN) {
326                LOGGER.error("updating resource {} is not authorized.", uri);
327                throw new ForbiddenException("updating resource " + uri + " is not authorized.");
328            } else if ( status.getStatusCode() == SC_NOT_FOUND) {
329                LOGGER.error("resource {} does not exist, cannot update", uri);
330                throw new NotFoundException("resource " + uri + " does not exist, cannot update");
331            } else if ( status.getStatusCode() == SC_CONFLICT) {
332                LOGGER.error("resource {} is locked", uri);
333                throw new FedoraException("resource is locked: " + uri);
334            } else {
335                LOGGER.error("error updating resource {}: {} {}", uri, status.getStatusCode(),
336                             status.getReasonPhrase());
337                throw new FedoraException("error updating resource " + uri + ": " + status.getStatusCode() + " " +
338                                          status.getReasonPhrase());
339            }
340
341            // update properties from server
342            httpHelper.loadProperties(this);
343
344        } catch (final FedoraException e) {
345            throw e;
346        } catch (final Exception e) {
347            LOGGER.error("could not encode URI parameter", e);
348            throw new FedoraException(e);
349        } finally {
350            patch.releaseConnection();
351        }
352    }
353
354    @Override
355    public void updateProperties(final InputStream updatedProperties, final String contentType)
356            throws FedoraException {
357
358        final HttpPut put = httpHelper.createTriplesPutMethod(getPropertiesPath(), updatedProperties, contentType);
359
360        try {
361            final HttpResponse response = httpHelper.execute( put );
362            final StatusLine status = response.getStatusLine();
363            final String uri = put.getURI().toString();
364
365            if ( status.getStatusCode() == SC_NO_CONTENT) {
366                LOGGER.debug("triples updated successfully for resource {}", uri);
367            } else if ( status.getStatusCode() == SC_FORBIDDEN) {
368                LOGGER.error("updating resource {} is not authorized.", uri);
369                throw new ForbiddenException("updating resource " + uri + " is not authorized.");
370            } else if ( status.getStatusCode() == SC_NOT_FOUND) {
371                LOGGER.error("resource {} does not exist, cannot update", uri);
372                throw new NotFoundException("resource " + uri + " does not exist, cannot update");
373            } else if ( status.getStatusCode() == SC_CONFLICT) {
374                LOGGER.error("resource {} is locked", uri);
375                throw new FedoraException("resource is locked: " + uri);
376            } else {
377                LOGGER.error("error updating resource {}: {} {}", uri, status.getStatusCode(),
378                             status.getReasonPhrase());
379                throw new FedoraException("error updating resource " + uri + ": " + status.getStatusCode() + " " +
380                                          status.getReasonPhrase());
381            }
382
383            // update properties from server
384            httpHelper.loadProperties(this);
385
386        } catch (final FedoraException e) {
387            throw e;
388        } catch (final Exception e) {
389            LOGGER.error("Error executing request", e);
390            throw new FedoraException(e);
391        } finally {
392            put.releaseConnection();
393        }
394    }
395
396    @Override
397    public boolean isWritable() {
398        final Collection<String> values = getPropertyValues(RdfLexicon.WRITABLE);
399        if (values != null && values.size() > 0) {
400            final Iterator<String> it = values.iterator();
401            return Boolean.parseBoolean(it.next());
402        }
403        return false;
404    }
405
406    @Override
407    public void createVersionSnapshot(final String label) throws FedoraException {
408        final HttpPost postVersion = httpHelper.createPostMethod(path + "/fcr:versions", null);
409        try {
410            postVersion.setHeader("Slug", label);
411            final HttpResponse response = httpHelper.execute(postVersion);
412            final StatusLine status = response.getStatusLine();
413            final String uri = postVersion.getURI().toString();
414
415            if ( status.getStatusCode() == SC_NO_CONTENT) {
416                LOGGER.debug("new version created for resource at {}", uri);
417            } else if ( status.getStatusCode() == SC_CONFLICT) {
418                LOGGER.debug("The label {} is in use by another version.", label);
419                throw new FedoraException("The label \"" + label + "\" is in use by another version.");
420            } else if ( status.getStatusCode() == SC_FORBIDDEN) {
421                LOGGER.error("updating resource {} is not authorized.", uri);
422                throw new ForbiddenException("updating resource " + uri + " is not authorized.");
423            } else if ( status.getStatusCode() == SC_NOT_FOUND) {
424                LOGGER.error("resource {} does not exist, cannot create version", uri);
425                throw new NotFoundException("resource " + uri + " does not exist, cannot create version");
426            } else {
427                LOGGER.error("error updating resource {}: {} {}", uri, status.getStatusCode(),
428                        status.getReasonPhrase());
429                throw new FedoraException("error updating resource " + uri + ": " + status.getStatusCode() + " " +
430                        status.getReasonPhrase());
431            }
432        } catch (IOException e) {
433            LOGGER.error("Error executing request", e);
434            throw new FedoraException(e);
435        } finally {
436            postVersion.releaseConnection();
437        }
438    }
439
440    /**
441     * Get the properties graph
442     *
443     * @return Graph containing properties for this resource
444     */
445    public Graph getGraph() {
446        return graph;
447    }
448
449    /**
450     * Update the properties graph
451     *
452     * @param graph graph to  add to this object
453    **/
454    public void setGraph( final Graph graph ) {
455        this.graph = graph;
456    }
457
458    private Date getDate(final Property property) {
459        Date date = null;
460        final Triple t = getTriple(subject, property);
461        if ( t != null ) {
462            final String dateValue = t.getObject().getLiteralValue().toString();
463            try {
464                date = dateFormat.parse(dateValue);
465            } catch (final ParseException e) {
466                LOGGER.debug("Invalid date format error: " + dateValue);
467            }
468        }
469        return date;
470    }
471
472    /**
473     * Return all the values of a property
474     *
475     * @param property The Property to get values for
476     * @return Collection of values
477     */
478    protected Collection<String> getPropertyValues(final Property property) {
479        final ExtendedIterator<Triple> iterator = graph.find(Node.ANY,
480                                                             property.asNode(),
481                                                             Node.ANY);
482        final Set<String> set = new HashSet<>();
483        while (iterator.hasNext()) {
484            final Node object = iterator.next().getObject();
485            if (object.isLiteral()) {
486                set.add(object.getLiteralValue().toString());
487            } else if (object.isURI()) {
488                set.add(object.getURI().toString());
489            } else {
490                set.add(object.toString());
491            }
492        }
493        return set;
494    }
495
496    protected Triple getTriple( final Node subject, final Property property ) {
497        final ExtendedIterator<Triple> it = graph.find( subject, property.asNode(), null );
498        try {
499            if ( it.hasNext() ) {
500                return it.next();
501            } else {
502                return null;
503            }
504        } finally {
505            it.close();
506        }
507    }
508
509    /**
510     * Gets the path to which properties of this resource may be accessed.
511     *
512     * @return string containing properties path
513     */
514    public String getPropertiesPath() {
515        return path;
516    }
517
518}