001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.http.api;
019
020import static java.util.Date.from;
021import static javax.ws.rs.core.Response.created;
022import static javax.ws.rs.core.Response.noContent;
023import static org.fcrepo.http.commons.domain.RDFMediaType.TEXT_PLAIN_WITH_CHARSET;
024import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_EXPIRES_HEADER;
025import static org.fcrepo.http.commons.session.TransactionConstants.EXPIRES_RFC_1123_FORMATTER;
026import static org.fcrepo.http.commons.session.TransactionConstants.TX_COMMIT_REL;
027import static org.fcrepo.http.commons.session.TransactionConstants.TX_PREFIX;
028import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_ID_PREFIX;
029import static org.slf4j.LoggerFactory.getLogger;
030
031import java.net.URI;
032import java.net.URISyntaxException;
033
034import javax.ws.rs.DELETE;
035import javax.ws.rs.GET;
036import javax.ws.rs.POST;
037import javax.ws.rs.PUT;
038import javax.ws.rs.Path;
039import javax.ws.rs.PathParam;
040import javax.ws.rs.core.Link;
041import javax.ws.rs.core.Response;
042import javax.ws.rs.core.Response.Status;
043
044import io.micrometer.core.annotation.Timed;
045import org.fcrepo.kernel.api.Transaction;
046import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
047import org.fcrepo.kernel.api.exception.TransactionClosedException;
048import org.fcrepo.kernel.api.exception.TransactionNotFoundException;
049import org.slf4j.Logger;
050import org.springframework.context.annotation.Scope;
051
052/**
053 * The rest interface for transaction management. The interfaces
054 * allows for creation, commit and rollback of transactions.
055 *
056 * @author awoods
057 * @author gregjan
058 * @author mohideen
059 */
060@Timed
061@Scope("prototype")
062@Path("/fcr:tx")
063public class Transactions extends FedoraBaseResource {
064
065    private static final Logger LOGGER = getLogger(Transactions.class);
066
067    /**
068     * Get the status of an existing transaction
069     *
070     * @param txId id of the transaction
071     * @return 204 no content if status retrieved, 410 gone if transaction doesn't exist.
072     */
073    @GET
074    @Path("{transactionId}")
075    public Response getTransactionStatus(@PathParam("transactionId") final String txId) {
076        // Retrieve the tx provided via the path
077        final Transaction tx;
078        try {
079            tx = txManager.get(txId);
080        } catch (final TransactionNotFoundException e) {
081            return Response.status(Status.NOT_FOUND).build();
082        } catch (final TransactionClosedException e) {
083            return Response.status(Status.GONE)
084                    .entity(e.getMessage())
085                    .type(TEXT_PLAIN_WITH_CHARSET)
086                    .build();
087        }
088
089        LOGGER.info("Checking transaction status'{}'", tx.getId());
090
091        return Response.status(Status.NO_CONTENT)
092                .header(ATOMIC_EXPIRES_HEADER, EXPIRES_RFC_1123_FORMATTER.format(tx.getExpires()))
093                .build();
094    }
095
096    /**
097     * Refresh an existing transaction
098     *
099     * @param txId id of the transaction
100     * @return 204 no content if successfully refreshed, 410 gone if transaction doesn't exist.
101     */
102    @POST
103    @Path("{transactionId}")
104    public Response refreshTransaction(@PathParam("transactionId") final String txId) {
105        // Retrieve the tx provided via the path
106        final Transaction tx;
107        try {
108            tx = txManager.get(txId);
109        } catch (final TransactionNotFoundException e) {
110            return Response.status(Status.NOT_FOUND).build();
111        } catch (final TransactionClosedException e) {
112            return Response.status(Status.GONE)
113                    .entity(e.getMessage()).type(TEXT_PLAIN_WITH_CHARSET)
114                    .build();
115        }
116
117        tx.refresh();
118        LOGGER.info("Refreshed transaction '{}'", tx.getId());
119
120        return Response.status(Status.NO_CONTENT)
121                .header(ATOMIC_EXPIRES_HEADER, EXPIRES_RFC_1123_FORMATTER.format(tx.getExpires()))
122                .build();
123    }
124
125    /**
126     * Create a new transaction resource and add it to the registry
127     *
128     * @return 201 with the transaction id and expiration date
129     * @throws URISyntaxException if URI syntax exception occurred
130     */
131    @POST
132    public Response createTransaction() throws URISyntaxException {
133        final Transaction tx = transaction();
134        tx.setShortLived(false);
135
136        LOGGER.info("Created transaction '{}'", tx.getId());
137        final var externalId = identifierConverter()
138                .toExternalId(FEDORA_ID_PREFIX + "/" + TX_PREFIX + tx.getId());
139        final var res = created(new URI(externalId));
140        res.expires(from(tx.getExpires()));
141
142        final var commitUri = URI.create(externalId);
143        final var commitLink = Link.fromUri(commitUri).rel(TX_COMMIT_REL).build();
144        res.links(commitLink);
145
146        return res.build();
147    }
148
149    /**
150     * Commit a transaction resource
151     *
152     * @param txId the transaction id
153     * @return 204
154     */
155    @PUT
156    @Path("{transactionId}")
157    public Response commit(@PathParam("transactionId") final String txId) {
158        try {
159            final Transaction transaction = txManager.get(txId);
160            LOGGER.info("Committing transaction '{}'", transaction.getId());
161            transaction.commit();
162            return noContent().build();
163        } catch (final TransactionNotFoundException e) {
164            return Response.status(Status.NOT_FOUND).build();
165        } catch (final TransactionClosedException e) {
166            return Response.status(Status.GONE)
167                    .entity(e.getMessage())
168                    .type(TEXT_PLAIN_WITH_CHARSET)
169                    .build();
170        } catch (final RepositoryRuntimeException e) {
171            return Response.status(Status.CONFLICT)
172                    .entity(e.getMessage())
173                    .type(TEXT_PLAIN_WITH_CHARSET)
174                    .build();
175        }
176    }
177
178    /**
179     * Rollback a transaction
180     *
181     * @param txId the transaction id
182     * @return 204
183     */
184    @DELETE
185    @Path("{transactionId}")
186    public Response rollback(@PathParam("transactionId") final String txId) {
187        try {
188            final Transaction transaction = txManager.get(txId);
189            LOGGER.info("Rollback transaction '{}'", transaction.getId());
190            transaction.rollback();
191            return noContent().build();
192        } catch (final TransactionNotFoundException e) {
193            return Response.status(Status.NOT_FOUND).build();
194        } catch (final TransactionClosedException e) {
195            return Response.status(Status.GONE)
196                    .entity(e.getMessage())
197                    .type(TEXT_PLAIN_WITH_CHARSET)
198                    .build();
199        } catch (final RepositoryRuntimeException e) {
200            return Response.status(Status.CONFLICT)
201                    .entity(e.getMessage())
202                    .type(TEXT_PLAIN_WITH_CHARSET)
203                    .build();
204        }
205    }
206
207}