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 // Nothing
} }
public static final String IDENTIFIER_PREFIX_ID = "ID-"; public static final String IDENTIFIER_PREFIX_ID = "ID-";
public static final String IDENTIFIER_PREFIX_UUID = "UUID-"; public static final String IDENTIFIER_PREFIX_UUID = "UUID-";

View File

@ -27,7 +27,7 @@ import org.libreccm.api.admin.categorization.DomainsApi;
*/ */
@ApplicationPath("/api/admin") @ApplicationPath("/api/admin")
public class AdminApi extends Application { public class AdminApi extends Application {
@Override @Override
public Set<Class<?>> getClasses() { public Set<Class<?>> getClasses() {
final Set<Class<?>> classes = new HashSet<>(); 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.CategorizationData;
import org.libreccm.api.admin.categorization.dto.CategoryData; import org.libreccm.api.admin.categorization.dto.CategoryData;
import org.libreccm.api.dto.ListView; import org.libreccm.api.dto.ListView;
import org.libreccm.categorization.CategoryManager;
import org.libreccm.categorization.CategoryRepository;
import org.libreccm.core.CoreConstants; import org.libreccm.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege; import org.libreccm.security.RequiresPrivilege;
import javax.enterprise.context.RequestScoped; import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
@ -47,6 +50,15 @@ import javax.ws.rs.core.MediaType;
@Path("/categories") @Path("/categories")
public class CategoriesApi { public class CategoriesApi {
@Inject
private CategoryManager categoryManager;
@Inject
private CategoryRepository categoryRepository;
@Inject
private CategorizationApiRepository repository;
@GET @GET
@Path("/{domainIdentifier}/{path:^[\\w\\-/]+$}") @Path("/{domainIdentifier}/{path:^[\\w\\-/]+$}")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -57,6 +69,8 @@ public class CategoriesApi {
@PathParam("domainIdentifier") final String domainIdentifierParam, @PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("path") final String categoryPathTokens @PathParam("path") final String categoryPathTokens
) { ) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -18,16 +18,29 @@
*/ */
package org.libreccm.api.admin.categorization; 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.DomainData;
import org.libreccm.api.admin.categorization.dto.DomainOwnershipData; import org.libreccm.api.admin.categorization.dto.DomainOwnershipData;
import org.libreccm.api.dto.ListView; 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.core.CoreConstants;
import org.libreccm.security.AuthorizationRequired; import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege; 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.List;
import java.util.stream.Collectors;
import javax.enterprise.context.RequestScoped; import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE; import javax.ws.rs.DELETE;
@ -39,16 +52,40 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam; import javax.ws.rs.PathParam;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam; 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.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
/** /**
* *
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a> * @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/ */
@RequestScoped @RequestScoped
@Path("/domains") @Path("/" + DomainsApi.RESOURCE_PATH)
public class DomainsApi { 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 @GET
@Path("/") @Path("/")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@ -59,7 +96,16 @@ public class DomainsApi {
@QueryParam("limit") @DefaultValue("20") final int limit, @QueryParam("limit") @DefaultValue("20") final int limit,
@QueryParam("offset") @DefaultValue("0") final int offset @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 @GET
@ -70,18 +116,29 @@ public class DomainsApi {
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public DomainData getDomain( public DomainData getDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam) { @PathParam("domainIdentifier") final String domainIdentifierParam) {
throw new UnsupportedOperationException();
return new DomainData(repository.findDomain(domainIdentifierParam));
} }
@POST @POST
@Path("/") @Path("/")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public ListView<DomainData> addDomain(final DomainData domainData) { public Response addDomain(final DomainData domainData) {
throw new UnsupportedOperationException(); final Domain domain = domainManager
.createDomain(
domainData.getDomainKey(), domainData.getRoot().getName()
);
return Response.created(
uriInfo
.getBaseUriBuilder()
.path(RESOURCE_PATH)
.path(domain.getDomainKey())
.build()
).build();
} }
@PUT @PUT
@ -91,10 +148,51 @@ public class DomainsApi {
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public DomainData updateDomain( public Response updateDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam, @PathParam("domainIdentifier") final String domainIdentifierParam,
final DomainData domainData) { 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 @DELETE
@ -102,21 +200,43 @@ public class DomainsApi {
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public DomainData deleteDomain( public Response deleteDomain(
@PathParam("domainIdentifier") final String domainIdentifierParam) { @PathParam("domainIdentifier") final String domainIdentifierParam
throw new UnsupportedOperationException(); ) {
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 @GET
@Path("/{domainIdentifier}/owners") @Path("/{domainIdentifier}/owners")
@Produces(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> getOwners( public ListView<DomainOwnershipData> getOwners(
@PathParam("domainIdentifier") final String domainIdentifierParam @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 @GET
@ -125,23 +245,116 @@ public class DomainsApi {
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> addOwner( public Response addOwner(
@PathParam("domainIdentifier") final String domainIdentifierParam, @PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("ownerIdentifier") final String ownerIdentifierParam @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}") @Path("/{domainIdentifier}/owners/{ownerIdentifier}")
@AuthorizationRequired @AuthorizationRequired
@RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN) @RequiresPrivilege(CoreConstants.PRIVILEGE_ADMIN)
@Transactional(Transactional.TxType.REQUIRED) @Transactional(Transactional.TxType.REQUIRED)
public List<DomainOwnershipData> removeOwner( public Response removeOwner(
@PathParam("domainIdentifier") final String domainIdentifierParam, @PathParam("domainIdentifier") final String domainIdentifierParam,
@PathParam("ownerIdentifier") final String ownerIdentifierParam @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 uuid;
private String uniqueId; private String uniqueId;
private String name;
/** /**
* Constructor for creating empty instances. * Constructor for creating empty instances.
@ -49,6 +51,7 @@ public class CategoryId {
categoryId = category.getObjectId(); categoryId = category.getObjectId();
uuid = category.getUuid(); uuid = category.getUuid();
uniqueId = category.getUniqueId(); uniqueId = category.getUniqueId();
name = category.getName();
} }
public long getCategoryId() { public long getCategoryId() {
@ -75,4 +78,14 @@ public class CategoryId {
this.uniqueId = uniqueId; 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 uuid;
private String context;
private CcmApplicationId owner; private CcmApplicationId owner;
private long ownerOrder;
public DomainOwnershipData() { public DomainOwnershipData() {
// Nothing // Nothing
} }
@ -40,7 +44,9 @@ public class DomainOwnershipData {
public DomainOwnershipData(final DomainOwnership ownership) { public DomainOwnershipData(final DomainOwnership ownership) {
ownershipId = ownership.getOwnershipId(); ownershipId = ownership.getOwnershipId();
uuid = ownership.getUuid(); uuid = ownership.getUuid();
context = ownership.getContext();
owner = new CcmApplicationId(ownership.getOwner()); owner = new CcmApplicationId(ownership.getOwner());
ownerOrder = ownership.getOwnerOrder();
} }
public long getOwnershipId() { public long getOwnershipId() {
@ -67,4 +73,20 @@ public class DomainOwnershipData {
this.owner = owner; 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( @NamedQuery(
name = "DomainOwnership.findByUuid", name = "DomainOwnership.findByUuid",
query = "SELECT o FROM DomainOwnership o WHERE o.uuid = :uuid" query = "SELECT o FROM DomainOwnership o WHERE o.uuid = :uuid"
) ),
,
@NamedQuery( @NamedQuery(
name = "DomainOwnership.findByOwnerAndDomain", name = "DomainOwnership.findByOwnerAndDomain",
query = "SELECT o FROM DomainOwnership o " 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, @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "uuid") property = "uuid")
@ -77,7 +89,7 @@ public class DomainOwnership implements Serializable, Exportable {
@Column(name = "OWNERSHIP_ID") @Column(name = "OWNERSHIP_ID")
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
private long ownershipId; private long ownershipId;
@Column(name = "UUID", unique = true, nullable = false) @Column(name = "UUID", unique = true, nullable = false)
@XmlElement(name = "uuid", namespace = CoreConstants.CORE_XML_NS) @XmlElement(name = "uuid", namespace = CoreConstants.CORE_XML_NS)
private String uuid; private String uuid;
@ -132,7 +144,7 @@ public class DomainOwnership implements Serializable, Exportable {
protected void setUuid(final String uuid) { protected void setUuid(final String uuid) {
this.uuid = uuid; this.uuid = uuid;
} }
public CcmApplication getOwner() { public CcmApplication getOwner() {
return owner; return owner;
} }

View File

@ -27,10 +27,13 @@ import javax.persistence.EntityGraph;
import javax.persistence.NoResultException; import javax.persistence.NoResultException;
import javax.persistence.TypedQuery; import javax.persistence.TypedQuery;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Stream;
/** /**
* A repository for executing CRUD operations on {@link Domain} objects. * A repository for executing CRUD operations on {@link Domain} objects.
@ -55,7 +58,7 @@ public class DomainRepository extends AbstractEntityRepository<Long, Domain> {
public String getIdAttributeName() { public String getIdAttributeName() {
return "objectId"; return "objectId";
} }
@Override @Override
public Long getIdOfEntity(final Domain entity) { public Long getIdOfEntity(final Domain entity) {
return entity.getObjectId(); 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) { public List<Domain> search(final String term) {
final TypedQuery<Domain> query = getEntityManager() final TypedQuery<Domain> query = getEntityManager()
.createNamedQuery("Domain.search", Domain.class); .createNamedQuery("Domain.search", Domain.class);