/* * Copyright 2015-2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.hawkular.inventory.rest; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.CONFLICT; import static javax.ws.rs.core.Response.Status.CREATED; import static javax.ws.rs.core.Response.Status.FORBIDDEN; import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; import static javax.ws.rs.core.Response.Status.NOT_FOUND; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import org.hawkular.inventory.api.Data; import org.hawkular.inventory.api.EntityAlreadyExistsException; import org.hawkular.inventory.api.EntityNotFoundException; import org.hawkular.inventory.api.Environments; import org.hawkular.inventory.api.Feeds; import org.hawkular.inventory.api.Inventory; import org.hawkular.inventory.api.MetadataPacks; import org.hawkular.inventory.api.MetricTypes; import org.hawkular.inventory.api.Metrics; import org.hawkular.inventory.api.OperationTypes; import org.hawkular.inventory.api.RelationAlreadyExistsException; import org.hawkular.inventory.api.Relationships; import org.hawkular.inventory.api.ResolvableToSingle; import org.hawkular.inventory.api.ResolvableToSingleEntity; import org.hawkular.inventory.api.ResourceTypes; import org.hawkular.inventory.api.Resources; import org.hawkular.inventory.api.Tenants; import org.hawkular.inventory.api.TransactionFrame; import org.hawkular.inventory.api.WriteInterface; import org.hawkular.inventory.api.model.AbstractElement; import org.hawkular.inventory.api.model.Blueprint; import org.hawkular.inventory.api.model.DataEntity; import org.hawkular.inventory.api.model.ElementBlueprintVisitor; import org.hawkular.inventory.api.model.Entity; import org.hawkular.inventory.api.model.Environment; import org.hawkular.inventory.api.model.Feed; import org.hawkular.inventory.api.model.MetadataPack; import org.hawkular.inventory.api.model.Metric; import org.hawkular.inventory.api.model.MetricType; import org.hawkular.inventory.api.model.OperationType; import org.hawkular.inventory.api.model.Relationship; import org.hawkular.inventory.api.model.Resource; import org.hawkular.inventory.api.model.ResourceType; import org.hawkular.inventory.paths.CanonicalPath; import org.hawkular.inventory.paths.ElementTypeVisitor; import org.hawkular.inventory.paths.Path; import org.hawkular.inventory.paths.SegmentType; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; /** * @author Lukas Krejci * @since 0.4.0 */ @javax.ws.rs.Path("/bulk") @Produces(APPLICATION_JSON) @Consumes(APPLICATION_JSON) @Api(value = "/bulk", description = "Endpoint for bulk operations on inventory entities", tags = "Bulk Create") public class RestBulk extends RestBase { private static CanonicalPath canonicalize(String path, CanonicalPath rootPath) { Path p; if (path == null || path.isEmpty()) { p = rootPath; } else { p = Path.fromPartiallyUntypedString(path, rootPath, rootPath, SegmentType.ANY_ENTITY); } if (p.isRelative()) { p = p.toRelativePath().applyTo(rootPath); } return p.toCanonicalPath(); } private static void putStatus(Map<ElementType, Map<CanonicalPath, Integer>> statuses, ElementType et, CanonicalPath cp, Integer status) { Map<CanonicalPath, Integer> typeStatuses = statuses.get(et); if (typeStatuses == null) { typeStatuses = new HashMap<>(); statuses.put(et, typeStatuses); } if (!typeStatuses.containsKey(cp)) { typeStatuses.put(cp, status); } if (status >= 200 && status < 300) { RestApiLogger.LOGGER.debugf("REST BULK created: %s", cp); } else { RestApiLogger.LOGGER.debugf("REST BULK failed (%d): %s", status, cp); } } private static boolean hasBeenProcessed(Map<ElementType, Map<CanonicalPath, Integer>> statuses, ElementType et, CanonicalPath cp) { Map<CanonicalPath, Integer> typeStatuses = statuses.get(et); return (typeStatuses != null && typeStatuses.containsKey(cp)); } private static String arrow(Relationship.Blueprint b) { switch (b.getDirection()) { case both: return "<-(" + b.getName() + ")->"; case outgoing: return "-(" + b.getName() + ")->"; case incoming: return "<-(" + b.getName() + ")-"; default: throw new IllegalStateException("Unhandled direction type: " + b.getDirection()); } } private static WriteInterface<?, ?, ?, ?> step(SegmentType elementClass, Class<?> nextType, ResolvableToSingle<?, ?> single) { return ElementTypeVisitor.accept(elementClass, new ElementTypeVisitor.Simple<WriteInterface<?, ?, ?, ?>, Void>() { @Override protected WriteInterface<?, ?, ?, ?> defaultAction(SegmentType elementType, Void parameter) { throw new IllegalArgumentException("Entity of type '" + nextType.getSimpleName() + "' cannot " + "be created under an entity of type '" + elementClass.getSimpleName() + "'."); } @Override public WriteInterface<?, ?, ?, ?> visitEnvironment(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitMetric(Void parameter) { return ((Environments.Single) single).metrics(); } @Override public WriteInterface<?, ?, ?, ?> visitResource(Void parameter) { return ((Environments.Single) single).resources(); } }, null); } @Override public WriteInterface<?, ?, ?, ?> visitFeed(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitMetric(Void parameter) { return ((Feeds.Single) single).metrics(); } @Override public WriteInterface<?, ?, ?, ?> visitMetricType(Void parameter) { return ((Feeds.Single) single).metricTypes(); } @Override public WriteInterface<?, ?, ?, ?> visitResource(Void parameter) { return ((Feeds.Single) single).resources(); } @Override public WriteInterface<?, ?, ?, ?> visitResourceType(Void parameter) { return ((Feeds.Single) single).resourceTypes(); } }, null); } @Override public WriteInterface<?, ?, ?, ?> visitOperationType(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitData(Void parameter) { return ((OperationTypes.Single) single).data(); } }, null); } @Override public WriteInterface<?, ?, ?, ?> visitResource(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitData(Void parameter) { return ((Resources.Single) single).data(); } @Override public WriteInterface<?, ?, ?, ?> visitResource(Void parameter) { return ((Resources.Single) single).resources(); } }, null); } @Override public WriteInterface<?, ?, ?, ?> visitResourceType(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitData(Void parameter) { return ((ResourceTypes.Single) single).data(); } @Override public WriteInterface<?, ?, ?, ?> visitOperationType(Void parameter) { return ((ResourceTypes.Single) single).operationTypes(); } }, null); } @Override public WriteInterface<?, ?, ?, ?> visitTenant(Void parameter) { return ElementTypeVisitor.accept(AbstractElement.segmentTypeFromType(nextType), new RejectingVisitor() { @Override public WriteInterface<?, ?, ?, ?> visitFeed(Void parameter) { return ((Tenants.Single) single).feeds(); } @Override public WriteInterface<?, ?, ?, ?> visitEnvironment(Void parameter) { return ((Tenants.Single) single).environments(); } @Override public WriteInterface<?, ?, ?, ?> visitMetricType(Void parameter) { return ((Tenants.Single) single).metricTypes(); } @Override public WriteInterface<?, ?, ?, ?> visitResourceType(Void parameter) { return ((Tenants.Single) single).resourceTypes(); } @Override public WriteInterface<?, ?, ?, ?> visitMetadataPack(Void parameter) { return ((Tenants.Single) single).metadataPacks(); } }, null); } class RejectingVisitor extends ElementTypeVisitor.Simple<WriteInterface<?, ?, ?, ?>, Void> { @Override protected WriteInterface<?, ?, ?, ?> defaultAction(SegmentType elementType, Void parameter) { throw new IllegalArgumentException( "Entity of type '" + nextType.getSimpleName() + "' cannot " + "be created under an entity of type '" + elementClass.getSimpleName() + "'."); } } }, null); } private static <E extends AbstractElement<?, ?>> ResolvableToSingle<? extends AbstractElement<?, ?>, ?> create (Blueprint b, WriteInterface<?, ?, ?, ?> wrt) { return b.accept( new ElementBlueprintVisitor.Simple<ResolvableToSingle<? extends AbstractElement<?, ?>, ?>, Void>() { @SuppressWarnings("unchecked") @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitData(DataEntity.Blueprint<?> data, Void parameter) { return ((Data.ReadWrite) wrt).create(data); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitEnvironment(Environment.Blueprint environment, Void parameter) { return ((Environments.ReadWrite) wrt).create(environment); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitFeed(Feed.Blueprint feed, Void parameter) { return ((Feeds.ReadWrite) wrt).create(feed); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitMetric(Metric.Blueprint metric, Void parameter) { return ((Metrics.ReadWrite) wrt).create(metric); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitMetricType(MetricType.Blueprint metricType, Void parameter) { return ((MetricTypes.ReadWrite) wrt).create(metricType); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitOperationType(OperationType.Blueprint operationType, Void parameter) { return ((OperationTypes.ReadWrite) wrt).create(operationType); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitResource(Resource.Blueprint resource, Void parameter) { return ((Resources.ReadWrite) wrt).create(resource); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitResourceType(ResourceType.Blueprint type, Void parameter) { return ((ResourceTypes.ReadWrite) wrt).create(type); } @Override public ResolvableToSingle<? extends AbstractElement<?, ?>, ?> visitMetadataPack(MetadataPack.Blueprint metadataPack, Void parameter) { return ((MetadataPacks.ReadWrite) wrt).create(metadataPack); } }, null); } @POST @javax.ws.rs.Path("/") @ApiOperation(value = "Bulk creation of new entities.", notes="The response body contains details about results of creation" + " of individual entities. The return value is a map where keys are types of entities created and values" + " are again maps where keys are the canonical paths of the entities to be created and values are HTTP" + " status codes - 201 OK, 400 if invalid path is supplied, 409 if the entity already exists on given path" + " or 500 in case of internal error.") @ApiResponses({ @ApiResponse(code = 201, message = "Entities successfully created"), }) public Response addEntities(@ApiParam("This is a map where keys are paths to the parents under which entities " + "should be created. The values are again maps where keys are one of [environment, resourceType, " + "metricType, operationType, feed, resource, metric, dataEntity, relationship] and values are arrays of " + "blueprints of entities of the corresponding types.") Map<String, Map<ElementType, List<Object>>> entities, @Context UriInfo uriInfo) { CanonicalPath rootPath = CanonicalPath.of().tenant(getTenantId()).get(); Map<ElementType, Map<CanonicalPath, Integer>> statuses = bulkCreate(entities, rootPath, uriInfo); return Response.status(CREATED).entity(statuses).build(); } private Map<ElementType, Map<CanonicalPath, Integer>> bulkCreate(Map<String, Map<ElementType, List<Object>>> entities, CanonicalPath rootPath, UriInfo uriInfo) { Map<ElementType, Map<CanonicalPath, Integer>> statuses = new HashMap<>(); TransactionFrame transaction = inventory(uriInfo).newTransactionFrame(); Inventory binv = transaction.boundInventory(); IdExtractor idExtractor = new IdExtractor(); try { for (Map.Entry<String, Map<ElementType, List<Object>>> e : entities.entrySet()) { Map<ElementType, List<Object>> allBlueprints = e.getValue(); CanonicalPath parentPath = canonicalize(e.getKey(), rootPath); RestApiLogger.LOGGER.tracef("Bulk creating under %s", parentPath); @SuppressWarnings("unchecked") ResolvableToSingle<? extends AbstractElement<?, ?>, ?> single = binv.inspect(parentPath, ResolvableToSingle.class); for (Map.Entry<ElementType, List<Object>> ee : allBlueprints.entrySet()) { ElementType elementType = ee.getKey(); List<Object> rawBlueprints = ee.getValue(); List<Blueprint> blueprints = deserializeBlueprints(elementType, rawBlueprints); if (elementType == ElementType.relationship) { bulkCreateRelationships(statuses, parentPath, (ResolvableToSingleEntity<?, ?>) single, elementType, blueprints); } else { bulkCreateEntity(statuses, idExtractor, parentPath, single, elementType, blueprints); } } RestApiLogger.LOGGER.tracef("Done bulk creating under %s", parentPath); } transaction.commit(); return statuses; } catch (Throwable t) { //note that the security resources are not yet created (they only get created after a successful commit) //so no "leftovers" are left behind in case of transaction rollback. transaction.rollback(); throw t; } } private List<Blueprint> deserializeBlueprints(ElementType elementType, List<Object> rawBlueprints) { return rawBlueprints.stream().map((o) -> { try { String js = getMapper().writeValueAsString(o); return (Blueprint) getMapper().reader(elementType.blueprintType).readValue(js); } catch (IOException e1) { throw new IllegalArgumentException("Failed to deserialize as " + elementType .blueprintType + " the following data: " + o, e1); } }).collect(Collectors.toList()); } private void bulkCreateEntity(Map<ElementType, Map<CanonicalPath, Integer>> statuses, IdExtractor idExtractor, CanonicalPath parentPath, ResolvableToSingle<? extends AbstractElement<?, ?>, ?> single, ElementType elementType, List<Blueprint> blueprints) { if (!parentPath.modified().canExtendTo(elementType.segmentType)) { RestApiLogger.LOGGER.debugf("Element type %s cannot be created under parent %s. Aborting bulk create.", elementType.segmentType, parentPath); putStatus(statuses, elementType, parentPath, BAD_REQUEST.getStatusCode()); return; } if (!canCreateUnderParent(elementType, parentPath, statuses)) { for (Blueprint b : blueprints) { String id = b.accept(idExtractor, null); putStatus(statuses, elementType, parentPath.extend(elementType.segmentType, id).get(), FORBIDDEN.getStatusCode()); } return; } for (Blueprint b : blueprints) { WriteInterface<?, ?, ?, ?> wrt = step(parentPath.getSegment().getElementType(), elementType .elementType, single); CanonicalPath provisionalChildPath = parentPath.extend(elementType.segmentType, b.accept(idExtractor, null)) .get(); boolean hasBeenProcessed = hasBeenProcessed(statuses, elementType, provisionalChildPath); if (hasBeenProcessed) { RestApiLogger.LOGGER.tracef("Skipping creation of %s. It seems to have been processed already", provisionalChildPath); // this entity has it's own record in the list with statuses so let's move to another one continue; } try { //this is cheap - the call to entity() right after create() doesn't fetch from the backend String childId = create(b, wrt).entity().getId(); CanonicalPath childPath = parentPath.extend(elementType.segmentType, childId).get(); RestApiLogger.LOGGER.tracef("Created %s", childPath); putStatus(statuses, elementType, childPath, CREATED.getStatusCode()); } catch (EntityAlreadyExistsException ex) { RestApiLogger.LOGGER.tracef("Entity already exists during bulk create: " + provisionalChildPath); putStatus(statuses, elementType, provisionalChildPath, CONFLICT.getStatusCode()); } catch (Exception ex) { RestApiLogger.LOGGER.failedToCreateBulkEntity(provisionalChildPath, ex); putStatus(statuses, elementType, provisionalChildPath, INTERNAL_SERVER_ERROR.getStatusCode()); } } } private void bulkCreateRelationships(Map<ElementType, Map<CanonicalPath, Integer>> statuses, CanonicalPath parentPath, ResolvableToSingleEntity<?, ?> single, ElementType elementType, List<Blueprint> blueprints) { if (!hasBeenCreatedInBulk(parentPath, statuses)) { if (!security.canAssociateFrom(parentPath)) { for (Blueprint b : blueprints) { Relationship.Blueprint rb = (Relationship.Blueprint) b; String id = parentPath.toString() + arrow(rb) + rb.getOtherEnd(); putStatus(statuses, elementType, parentPath.extend(elementType.segmentType, id).get(), FORBIDDEN.getStatusCode()); } return; } } for (Blueprint b : blueprints) { Relationship.Blueprint rb = (Relationship.Blueprint) b; String fakeId = parentPath.toString() + arrow(rb) + rb.getOtherEnd().toString(); CanonicalPath cPath = CanonicalPath.of().relationship(fakeId).get(); boolean hasBeenProcessed = hasBeenProcessed(statuses, elementType, cPath); if (hasBeenProcessed) { // this relationship has it's own record in the list with statuses so let's move to another one continue; } try { Relationships.Single rel = single.relationships(rb.getDirection()) .linkWith(rb.getName(), rb.getOtherEnd(), rb.getProperties()); putStatus(statuses, elementType, cPath, CREATED.getStatusCode()); } catch (EntityNotFoundException ex) { putStatus(statuses, elementType, cPath, NOT_FOUND.getStatusCode()); } catch (RelationAlreadyExistsException ex) { putStatus(statuses, elementType, cPath, CONFLICT.getStatusCode()); } catch (Exception ex) { putStatus(statuses, elementType, cPath, INTERNAL_SERVER_ERROR.getStatusCode()); } } } private boolean canCreateUnderParent(ElementType elementType, CanonicalPath parentPath, Map<ElementType, Map<CanonicalPath, Integer>> statuses) { if (hasBeenCreatedInBulk(parentPath, statuses)) { //the parent has been created in the bulk request. I.e. we're still in a transaction that's creating the //entities and therefore the security resources have not been created for such elements yet. We assume that //if we were allowed to create the parent, we can also create its child. return true; } switch (elementType) { case dataEntity: return security.canUpdate(parentPath); case relationship: throw new IllegalArgumentException("Cannot create anything under a relationship."); default: return security.canCreate(elementType.elementType).under(parentPath); } } private boolean hasBeenCreatedInBulk(CanonicalPath elementPath, Map<ElementType, Map<CanonicalPath, Integer>> statuses) { Map<CanonicalPath, Integer> elementsOfType = statuses.get( ElementType.ofSegmentType(elementPath.getSegment().getElementType())); if (elementsOfType == null) { return false; } Integer status = elementsOfType.get(elementPath); if (status == null) { return false; } return status == 201 || status == 204; } public enum ElementType { environment(Environment.class, Environment.Blueprint.class, SegmentType.e), resourceType(ResourceType.class, ResourceType.Blueprint.class, SegmentType.rt), metricType(MetricType.class, MetricType.Blueprint.class, SegmentType.mt), operationType(OperationType.class, OperationType.Blueprint.class, SegmentType.ot), feed(Feed.class, Feed.Blueprint.class, SegmentType.f), metric(Metric.class, Metric.Blueprint.class, SegmentType.m), resource(Resource.class, Resource.Blueprint.class, SegmentType.r), dataEntity(DataEntity.class, DataEntity.Blueprint.class, SegmentType.d), metadataPack(MetadataPack.class, MetadataPack.Blueprint.class, SegmentType.mp), relationship(Relationship.class, Relationship.Blueprint.class, SegmentType.r); final Class<? extends AbstractElement<?, ?>> elementType; final Class<? extends Blueprint> blueprintType; final SegmentType segmentType; ElementType(Class<? extends AbstractElement<?, ?>> elementType, Class<? extends Blueprint> blueprintType, SegmentType segmentType) { this.elementType = elementType; this.blueprintType = blueprintType; this.segmentType = segmentType; } public static ElementType ofSegmentType(SegmentType type) { for (ElementType et : ElementType.values()) { if (et.segmentType.equals(type)) { return et; } } return null; } public static ElementType ofBlueprintType(Class<?> type) { for (ElementType et : ElementType.values()) { if (et.blueprintType.equals(type)) { return et; } } return null; } } public static class IdExtractor extends ElementBlueprintVisitor.Simple<String, Void> { @Override protected String defaultAction(Object blueprint, Void parameter) { return ((Entity.Blueprint) blueprint).getId(); } @Override public String visitData(DataEntity.Blueprint<?> data, Void parameter) { return data.getRole().name(); } @Override public String visitMetadataPack(MetadataPack.Blueprint metadataPack, Void parameter) { return "<metadata-pack>"; } @Override public String visitRelationship(Relationship.Blueprint relationship, Void parameter) { return arrow(relationship) + relationship.getOtherEnd().toString(); } } }