RESTful API for managing Domains

Former-commit-id: 10da2f6ef6
restapi
Jens Pelzetter 2020-06-06 16:15:15 +02:00
parent 4a4e4beb78
commit 407384a75a
8 changed files with 355 additions and 30 deletions

View File

@ -28,6 +28,7 @@ public final class ApiConstants {
// Nothing
}
public static final String IDENTIFIER_PREFIX_ID = "ID-";
public static final String IDENTIFIER_PREFIX_UUID = "UUID-";

View File

@ -27,7 +27,7 @@ import org.libreccm.api.admin.categorization.DomainsApi;
*/
@ApplicationPath("/api/admin")
public class AdminApi extends Application {
@Override
public Set<Class<?>> getClasses() {
final Set<Class<?>> classes = new HashSet<>();

View File

@ -21,11 +21,14 @@ package org.libreccm.api.admin.categorization;
import org.libreccm.api.admin.categorization.dto.CategorizationData;
import org.libreccm.api.admin.categorization.dto.CategoryData;
import org.libreccm.api.dto.ListView;
import org.libreccm.categorization.CategoryManager;
import org.libreccm.categorization.CategoryRepository;
import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@ -47,6 +50,15 @@ import javax.ws.rs.core.MediaType;
@Path("/categories")
public class CategoriesApi {
@Inject
private CategoryManager categoryManager;
@Inject
private CategoryRepository categoryRepository;
@Inject
private CategorizationApiRepository repository;
@GET
@Path("/{domainIdentifier}/{path:^[\\w\\-/]+$}")
@Produces(MediaType.APPLICATION_JSON)
@ -57,6 +69,8 @@ public class CategoriesApi {
@PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("path") final String categoryPathTokens
) {
throw new UnsupportedOperationException();
}

View File

@ -18,16 +18,29 @@
*/
package org.libreccm.api.admin.categorization;
import org.libreccm.api.Identifier;
import org.libreccm.api.IdentifierParser;
import org.libreccm.api.admin.categorization.dto.DomainData;
import org.libreccm.api.admin.categorization.dto.DomainOwnershipData;
import org.libreccm.api.dto.ListView;
import org.libreccm.categorization.Domain;
import org.libreccm.categorization.DomainManager;
import org.libreccm.categorization.DomainRepository;
import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import org.libreccm.web.ApplicationRepository;
import org.libreccm.web.CcmApplication;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
@ -39,16 +52,40 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path("/domains")
@Path("/" + DomainsApi.RESOURCE_PATH)
public class DomainsApi {
public static final String RESOURCE_PATH = "domains";
@Context
private UriInfo uriInfo;
@Inject
private ApplicationRepository applicationRepository;
@Inject
private DomainManager domainManager;
@Inject
private DomainRepository domainRepository;
@Inject
private CategorizationApiRepository repository;
@Inject
private IdentifierParser identifierParser;
@GET
@Path("/")
@Produces(MediaType.APPLICATION_JSON)
@ -59,7 +96,16 @@ public class DomainsApi {
@QueryParam("limit") @DefaultValue("20") final int limit,
@QueryParam("offset") @DefaultValue("0") final int offset
) {
throw new UnsupportedOperationException();
return new ListView<>(
domainRepository
.findAll(limit, offset)
.stream()
.map(DomainData::new)
.collect(Collectors.toList()),
domainRepository.countAll(),
limit,
offset
);
}
@GET
@ -70,18 +116,29 @@ public class DomainsApi {
@Transactional(Transactional.TxType.REQUIRED)
public DomainData getDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam) {
throw new UnsupportedOperationException();
return new DomainData(repository.findDomain(domainIdentifierParam));
}
@POST
@Path("/")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public ListView<DomainData> addDomain(final DomainData domainData) {
throw new UnsupportedOperationException();
public Response addDomain(final DomainData domainData) {
final Domain domain = domainManager
.createDomain(
domainData.getDomainKey(), domainData.getRoot().getName()
);
return Response.created(
uriInfo
.getBaseUriBuilder()
.path(RESOURCE_PATH)
.path(domain.getDomainKey())
.build()
).build();
}
@PUT
@ -91,10 +148,51 @@ public class DomainsApi {
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public DomainData updateDomain(
public Response updateDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam,
final DomainData domainData) {
throw new UnsupportedOperationException();
final Domain domain = repository.findDomain(domainIdentifierParam);
boolean updated = false;
if (!domainData.getDescription().equals(domain.getDescription())) {
domain.setDescription(domainData.getDescription());
updated = true;
}
final String domainReleased = DateTimeFormatter.ISO_DATE_TIME.format(
domain.getReleased().toInstant()
);
if (!domainData.getReleased().equals(domainReleased)) {
final TemporalAccessor released = DateTimeFormatter.ISO_DATE_TIME
.parse(domainData.getReleased());
final Instant relasedInstant = Instant.from(released);
domain.setReleased(Date.from(relasedInstant));
updated = true;
}
if (!domainData.getTitle().equals(domain.getTitle())) {
domain.setTitle(domainData.getTitle());
updated = true;
}
if (!domainData.getVersion().equals(domain.getVersion())) {
domain.setVersion(domainData.getVersion());
updated = true;
}
if (updated) {
domainRepository.save(domain);
}
return Response
.ok(
String.format(
"Domain %s updated successfully.", domain.getDomainKey()
)
)
.build();
}
@DELETE
@ -102,21 +200,43 @@ public class DomainsApi {
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public DomainData deleteDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam) {
throw new UnsupportedOperationException();
public Response deleteDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam
) {
final Domain domain = repository.findDomain(domainIdentifierParam);
final String domainKey = domain.getDomainKey();
domainRepository.delete(domain);
return Response
.ok(
String.format(
"Domain %s deleted successfully.", domainKey
)
).build();
}
@GET
@Path("/{domainIdentifier}/owners")
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> getOwners(
@PathParam("domainIdentifier") final String domainIdentifierParam
public ListView<DomainOwnershipData> getOwners(
@PathParam("domainIdentifier") final String domainIdentifierParam,
@QueryParam("limit") @DefaultValue("20") final int limit,
@QueryParam("offset") @DefaultValue("0") final int offset
) {
throw new UnsupportedOperationException();
final Domain domain = repository.findDomain(domainIdentifierParam);
return new ListView<>(
domainRepository
.findOwnersOfDomainAsStream(domain, limit, offset)
.map(DomainOwnershipData::new)
.collect(Collectors.toList()),
domainRepository.countOwnersOfDomain(domain),
limit,
offset
);
}
@GET
@ -125,23 +245,116 @@ public class DomainsApi {
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> addOwner(
public Response addOwner(
@PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("ownerIdentifier") final String ownerIdentifierParam
) {
throw new UnsupportedOperationException();
final Domain domain = repository.findDomain(domainIdentifierParam);
final CcmApplication owner;
final Identifier ownerIdentifier = identifierParser.parseIdentifier(
ownerIdentifierParam
);
switch (ownerIdentifier.getType()) {
case ID:
owner = applicationRepository
.findById(Long.parseLong(ownerIdentifier.getIdentifier()))
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No CcmApplication with ID %s found.",
ownerIdentifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
break;
case UUID:
owner = applicationRepository
.findByUuid(ownerIdentifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No CcmApplication with UUID %s found.",
ownerIdentifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
break;
default:
throw new WebApplicationException(
"CcmApplications can only be identified by ID or UUID.",
Response.Status.BAD_REQUEST
);
}
domainManager.addDomainOwner(owner, domain);
return Response.ok(
String.format(
"CcmApplication %s is now an owner of the Domain %s",
owner.getPrimaryUrl(),
domain.getDomainKey()
)
).build();
}
@DELETE
@DELETE
@Path("/{domainIdentifier}/owners/{ownerIdentifier}")
@AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> removeOwner(
public Response removeOwner(
@PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("ownerIdentifier") final String ownerIdentifierParam
) {
throw new UnsupportedOperationException();
final Domain domain = repository.findDomain(domainIdentifierParam);
final CcmApplication owner;
final Identifier ownerIdentifier = identifierParser.parseIdentifier(
ownerIdentifierParam
);
switch (ownerIdentifier.getType()) {
case ID:
owner = applicationRepository
.findById(Long.parseLong(ownerIdentifier.getIdentifier()))
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No CcmApplication with ID %s found.",
ownerIdentifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
break;
case UUID:
owner = applicationRepository
.findByUuid(ownerIdentifier.getIdentifier())
.orElseThrow(
() -> new WebApplicationException(
String.format(
"No CcmApplication with UUID %s found.",
ownerIdentifier.getIdentifier()
),
Response.Status.NOT_FOUND
)
);
break;
default:
throw new WebApplicationException(
"CcmApplications can only be identified by ID or UUID.",
Response.Status.BAD_REQUEST
);
}
domainManager.removeDomainOwner(owner, domain);
return Response.ok(
String.format(
"CcmApplication %s no longer owns Domain %s",
owner.getPrimaryUrl(),
domain.getDomainKey()
)
).build();
}
}
}

View File

@ -32,6 +32,8 @@ public class CategoryId {
private String uuid;
private String uniqueId;
private String name;
/**
* Constructor for creating empty instances.
@ -49,6 +51,7 @@ public class CategoryId {
categoryId = category.getObjectId();
uuid = category.getUuid();
uniqueId = category.getUniqueId();
name = category.getName();
}
public long getCategoryId() {
@ -75,4 +78,14 @@ public class CategoryId {
this.uniqueId = uniqueId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@ -31,8 +31,12 @@ public class DomainOwnershipData {
private String uuid;
private String context;
private CcmApplicationId owner;
private long ownerOrder;
public DomainOwnershipData() {
// Nothing
}
@ -40,7 +44,9 @@ public class DomainOwnershipData {
public DomainOwnershipData(final DomainOwnership ownership) {
ownershipId = ownership.getOwnershipId();
uuid = ownership.getUuid();
context = ownership.getContext();
owner = new CcmApplicationId(ownership.getOwner());
ownerOrder = ownership.getOwnerOrder();
}
public long getOwnershipId() {
@ -67,4 +73,20 @@ public class DomainOwnershipData {
this.owner = owner;
}
public String getContext() {
return context;
}
public void setContext(final String context) {
this.context = context;
}
public long getOwnerOrder() {
return ownerOrder;
}
public void setOwnerOrder(final long ownerOrder) {
this.ownerOrder = ownerOrder;
}
}

View File

@ -57,12 +57,24 @@ import javax.xml.bind.annotation.XmlElement;
@NamedQuery(
name = "DomainOwnership.findByUuid",
query = "SELECT o FROM DomainOwnership o WHERE o.uuid = :uuid"
)
,
),
@NamedQuery(
name = "DomainOwnership.findByOwnerAndDomain",
query = "SELECT o FROM DomainOwnership o "
+ "WHERE o.owner = :owner AND o.domain = :domain")
+ "WHERE o.owner = :owner AND o.domain = :domain"
),
@NamedQuery(
name = "DomainOwnership.findForDomain",
query = "SELECT o FROM DomainOwnership o "
+ "WHERE o.domain = :domain "
+ "ORDER BY o.ownerOrder"
),
@NamedQuery(
name = "DomainOwnershop.countForDomain",
query = "SELECT COUNT(o) "
+ "FROM DomainOwnership o "
+ "WHERE o.domain = :domain"
)
})
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "uuid")
@ -77,7 +89,7 @@ public class DomainOwnership implements Serializable, Exportable {
@Column(name = "OWNERSHIP_ID")
@GeneratedValue(strategy = GenerationType.AUTO)
private long ownershipId;
@Column(name = "UUID", unique = true, nullable = false)
@XmlElement(name = "uuid", namespace = CoreConstants.CORE_XML_NS)
private String uuid;
@ -132,7 +144,7 @@ public class DomainOwnership implements Serializable, Exportable {
protected void setUuid(final String uuid) {
this.uuid = uuid;
}
public CcmApplication getOwner() {
return owner;
}

View File

@ -27,10 +27,13 @@ import javax.persistence.EntityGraph;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.transaction.Transactional;
import java.net.URI;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Stream;
/**
* A repository for executing CRUD operations on {@link Domain} objects.
@ -55,7 +58,7 @@ public class DomainRepository extends AbstractEntityRepository<Long, Domain> {
public String getIdAttributeName() {
return "objectId";
}
@Override
public Long getIdOfEntity(final Domain entity) {
return entity.getObjectId();
@ -138,6 +141,53 @@ public class DomainRepository extends AbstractEntityRepository<Long, Domain> {
}
}
public List<DomainOwnership> findOwnersOfDomain(
final Domain domain, final int limit, final int offset
) {
return getEntityManager()
.createNamedQuery(
"DomainOwnership.findForDomain", DomainOwnership.class
)
.setParameter(
"domain",
Objects.requireNonNull(
domain, "Can't find owners for domain null."
)
)
.setMaxResults(limit)
.setFirstResult(offset)
.getResultList();
}
public Stream<DomainOwnership> findOwnersOfDomainAsStream(
final Domain domain, final int limit, final int offset
) {
return getEntityManager()
.createNamedQuery(
"DomainOwnership.findForDomain", DomainOwnership.class
)
.setParameter(
"domain",
Objects.requireNonNull(
domain, "Can't find owners for domain null."
)
)
.setMaxResults(limit)
.setFirstResult(offset)
.getResultStream();
}
public long countOwnersOfDomain(final Domain domain) {
return getEntityManager()
.createNamedQuery("DomainOwnershop.countForDomain", Long.class)
.setParameter(
"domain",
Objects.requireNonNull(
domain, "Can't find owners for domain null."
)
).getSingleResult();
}
public List<Domain> search(final String term) {
final TypedQuery<Domain> query = getEntityManager()
.createNamedQuery("Domain.search", Domain.class);