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}