diff --git a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java index f0aae0c..e4f3dfd 100644 --- a/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java +++ b/intg/src/main/java/org/apache/atlas/AtlasErrorCode.java @@ -53,6 +53,8 @@ public enum AtlasErrorCode { PATCH_FOR_UNKNOWN_TYPE(400, "ATLAS40023E", "{0} - patch references unknown type {1}"), PATCH_INVALID_DATA(400, "ATLAS40024E", "{0} - patch data is invalid for type {1}"), TYPE_NAME_INVALID_FORMAT(400, "ATLAS40025E", "{0}: invalid name for {1}. Names must consist of a letter followed by a sequence of letter, number, or '_' characters"), + INVALID_PARAMETERS(400, "ATLAS40025E", "invalid parameters: {0}"), + CLASSIFICATION_ALREADY_ASSOCIATED(400, "ATLAS40026E", "instance {0} already is associated with classification {1}"), // All Not found enums go here TYPE_NAME_NOT_FOUND(404, "ATLAS4041E", "Given typename {0} was invalid"), diff --git a/intg/src/main/java/org/apache/atlas/model/instance/ClassificationAssociateRequest.java b/intg/src/main/java/org/apache/atlas/model/instance/ClassificationAssociateRequest.java new file mode 100644 index 0000000..8d0fac6 --- /dev/null +++ b/intg/src/main/java/org/apache/atlas/model/instance/ClassificationAssociateRequest.java @@ -0,0 +1,100 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.atlas.model.instance; + +import org.apache.atlas.model.typedef.AtlasBaseTypeDef; +import org.codehaus.jackson.annotate.JsonAutoDetect; +import org.codehaus.jackson.annotate.JsonIgnoreProperties; +import org.codehaus.jackson.map.annotate.JsonSerialize; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.NONE; +import static org.codehaus.jackson.annotate.JsonAutoDetect.Visibility.PUBLIC_ONLY; + +import java.util.List; +import java.util.Objects; + +@JsonAutoDetect(getterVisibility=PUBLIC_ONLY, setterVisibility=PUBLIC_ONLY, fieldVisibility=NONE) +@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown=true) +@XmlRootElement +@XmlAccessorType(XmlAccessType.PROPERTY) +public class ClassificationAssociateRequest { + private AtlasClassification classification; + private List<String> entityGuids; + + public ClassificationAssociateRequest() { + this(null, null); + } + + public ClassificationAssociateRequest(List<String> entityGuids, AtlasClassification classification) { + setEntityGuids(entityGuids); + setClassification(classification); + } + + public AtlasClassification getClassification() { return classification; } + + public void setClassification(AtlasClassification classification) { this.classification = classification; } + + public List<String> getEntityGuids() { return entityGuids; } + + public void setEntityGuids(List<String> entityGuids) { this.entityGuids = entityGuids; } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + + if (o == null || getClass() != o.getClass()) { return false; } + + ClassificationAssociateRequest that = (ClassificationAssociateRequest) o; + + return Objects.equals(classification, that.classification) && Objects.equals(entityGuids, that.entityGuids); + } + + @Override + public int hashCode() { + return Objects.hash(classification, entityGuids); + } + + public StringBuilder toString(StringBuilder sb) { + if (sb == null) { + sb = new StringBuilder(); + } + + sb.append("ClassificationAssociateRequest{"); + sb.append("classification='"); + if (classification != null) { + classification.toString(sb); + } + sb.append(", entityGuids=["); + AtlasBaseTypeDef.dumpObjects(entityGuids, sb); + sb.append("]"); + sb.append('}'); + + return sb; + } + + @Override + public String toString() { + return toString(new StringBuilder()).toString(); + } + +} diff --git a/release-log.txt b/release-log.txt index 55ac3da..760f4c3 100644 --- a/release-log.txt +++ b/release-log.txt @@ -9,6 +9,7 @@ ATLAS-1060 Add composite indexes for exact match performance improvements for al ATLAS-1127 Modify creation and modification timestamps to Date instead of Long(sumasai) ALL CHANGES: +ATLAS-1478 REST API to add classification to multiple entities (svimal2106 via mneethiraj) ATLAS-1490 added methods to get sub-types of entity and classification types (mneethiraj) ATLAS-1437 UI update to disallow tag association changes to deleted entities (Kalyanikashikar via mneethiraj) ATLAS-1352 fix for error in redirecting to Knox gateway URL (nixonrodrigues via mneethiraj) diff --git a/repository/src/main/java/org/apache/atlas/repository/MetadataRepository.java b/repository/src/main/java/org/apache/atlas/repository/MetadataRepository.java index 886a8d1..1d61ea8 100755 --- a/repository/src/main/java/org/apache/atlas/repository/MetadataRepository.java +++ b/repository/src/main/java/org/apache/atlas/repository/MetadataRepository.java @@ -146,6 +146,14 @@ public interface MetadataRepository { void addTrait(String guid, ITypedStruct traitInstance) throws RepositoryException; /** + * Adds a new trait to a list of entities represented by their respective guids + * @param entityGuids list of globally unique identifier for the entities + * @param traitInstance trait instance that needs to be added to entities + * @throws RepositoryException + */ + void addTrait(List<String> entityGuids, ITypedStruct traitInstance) throws RepositoryException; + + /** * Deletes a given trait from an existing entity represented by a guid. * * @param guid globally unique identifier for the entity diff --git a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedMetadataRepository.java b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedMetadataRepository.java index b9671b2..0c80aed 100755 --- a/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedMetadataRepository.java +++ b/repository/src/main/java/org/apache/atlas/repository/graph/GraphBackedMetadataRepository.java @@ -225,6 +225,26 @@ public class GraphBackedMetadataRepository implements MetadataRepository { return GraphHelper.getTraitNames(instanceVertex); } + /** + * Adds a new trait to the list of entities represented by their respective guids + * @param entityGuids list of globally unique identifier for the entities + * @param traitInstance trait instance that needs to be added to entities + * @throws RepositoryException + */ + @Override + @GraphTransaction + public void addTrait(List<String> entityGuids, ITypedStruct traitInstance) throws RepositoryException { + Preconditions.checkNotNull(entityGuids, "entityGuids list cannot be null"); + Preconditions.checkNotNull(traitInstance, "Trait instance cannot be null"); + + if (LOG.isDebugEnabled()) { + LOG.debug("Adding a new trait={} for entities={}", traitInstance.getTypeName(), entityGuids); + } + + for (String entityGuid : entityGuids) { + addTraitImpl(entityGuid, traitInstance); + } + } /** * Adds a new trait to an existing entity represented by a guid. @@ -236,7 +256,13 @@ public class GraphBackedMetadataRepository implements MetadataRepository { @Override @GraphTransaction public void addTrait(String guid, ITypedStruct traitInstance) throws RepositoryException { + Preconditions.checkNotNull(guid, "guid cannot be null"); Preconditions.checkNotNull(traitInstance, "Trait instance cannot be null"); + + addTraitImpl(guid, traitInstance); + } + + private void addTraitImpl(String guid, ITypedStruct traitInstance) throws RepositoryException { final String traitName = traitInstance.getTypeName(); if (LOG.isDebugEnabled()) { @@ -259,7 +285,7 @@ public class GraphBackedMetadataRepository implements MetadataRepository { GraphHelper.setProperty(instanceVertex, Constants.MODIFICATION_TIMESTAMP_PROPERTY_KEY, RequestContext.get().getRequestTime()); GraphHelper.setProperty(instanceVertex, Constants.MODIFIED_BY_KEY, RequestContext.get().getUser()); - + } catch (RepositoryException e) { throw e; } catch (Exception e) { diff --git a/repository/src/main/java/org/apache/atlas/services/DefaultMetadataService.java b/repository/src/main/java/org/apache/atlas/services/DefaultMetadataService.java index 8307835..35a489f 100755 --- a/repository/src/main/java/org/apache/atlas/services/DefaultMetadataService.java +++ b/repository/src/main/java/org/apache/atlas/services/DefaultMetadataService.java @@ -71,7 +71,6 @@ import org.codehaus.jettison.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -568,6 +567,39 @@ public class DefaultMetadataService implements MetadataService, ActiveStateChang } /** + * Adds a new trait to the list of existing entities represented by their respective guids + * @param entityGuids list of guids of entities + * @param traitInstance trait instance json that needs to be added to entities + * @throws AtlasException + */ + @Override + public void addTrait(List<String> entityGuids, ITypedStruct traitInstance) throws AtlasException { + Preconditions.checkNotNull(entityGuids, "entityGuids list cannot be null"); + Preconditions.checkNotNull(traitInstance, "Trait instance cannot be null"); + + final String traitName = traitInstance.getTypeName(); + + // ensure trait type is already registered with the TypeSystem + if (!typeSystem.isRegistered(traitName)) { + String msg = String.format("trait=%s should be defined in type system before it can be added", traitName); + LOG.error(msg); + throw new TypeNotFoundException(msg); + } + + //ensure trait is not already registered with any of the given entities + for (String entityGuid : entityGuids) { + Preconditions.checkArgument(!getTraitNames(entityGuid).contains(traitName), + "trait=%s is already defined for entity=%s", traitName, entityGuid); + } + + repository.addTrait(entityGuids, traitInstance); + + for (String entityGuid : entityGuids) { + onTraitAddedToEntity(repository.getEntityDefinition(entityGuid), traitInstance); + } + } + + /** * Adds a new trait to an existing entity represented by a guid. * * @param guid globally unique identifier for the entity diff --git a/server-api/src/main/java/org/apache/atlas/services/MetadataService.java b/server-api/src/main/java/org/apache/atlas/services/MetadataService.java index e653184..d5d8d9b 100644 --- a/server-api/src/main/java/org/apache/atlas/services/MetadataService.java +++ b/server-api/src/main/java/org/apache/atlas/services/MetadataService.java @@ -28,6 +28,7 @@ import org.apache.atlas.typesystem.Referenceable; import org.apache.atlas.typesystem.Struct; import org.apache.atlas.typesystem.IStruct; import org.apache.atlas.typesystem.types.cache.TypeCache; +import org.apache.atlas.utils.ParamChecker; import org.codehaus.jettison.json.JSONObject; import java.util.List; @@ -221,6 +222,15 @@ public interface MetadataService { */ void addTrait(String guid, ITypedStruct traitInstance) throws AtlasException; + + /** + * Adds a new trait to a list of existing entities represented by their respective guids + * @param entityGuids list of guids of entities + * @param traitInstance trait instance json that needs to be added to entities + * @throws AtlasException + */ + void addTrait(List<String> entityGuids, ITypedStruct traitInstance) throws AtlasException; + /** * Create a typed trait instance. * diff --git a/webapp/src/main/java/org/apache/atlas/web/rest/EntitiesREST.java b/webapp/src/main/java/org/apache/atlas/web/rest/EntitiesREST.java index 21f8977..5107767 100644 --- a/webapp/src/main/java/org/apache/atlas/web/rest/EntitiesREST.java +++ b/webapp/src/main/java/org/apache/atlas/web/rest/EntitiesREST.java @@ -22,15 +22,19 @@ import org.apache.atlas.AtlasErrorCode; import org.apache.atlas.AtlasException; import org.apache.atlas.exception.AtlasBaseException; import org.apache.atlas.model.SearchFilter; +import org.apache.atlas.model.instance.AtlasClassification; import org.apache.atlas.model.instance.AtlasEntity; import org.apache.atlas.model.instance.AtlasEntityHeader; +import org.apache.atlas.model.instance.ClassificationAssociateRequest; import org.apache.atlas.model.instance.EntityMutationResponse; import org.apache.atlas.repository.store.graph.AtlasEntityStore; import org.apache.atlas.services.MetadataService; import org.apache.atlas.typesystem.ITypedReferenceableInstance; +import org.apache.atlas.typesystem.ITypedStruct; import org.apache.atlas.web.adapters.AtlasInstanceRestAdapters; import org.apache.atlas.web.util.Servlets; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +50,7 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; @@ -184,6 +189,37 @@ public class EntitiesREST { return entityHeaders; } + /** + * Bulk API to associate a tag to multiple entities + * + */ + @POST + @Path("/classification") + @Consumes({Servlets.JSON_MEDIA_TYPE, MediaType.APPLICATION_JSON}) + @Produces(Servlets.JSON_MEDIA_TYPE) + public void addClassification(ClassificationAssociateRequest request) throws AtlasBaseException { + AtlasClassification classification = request == null ? null : request.getClassification(); + List<String> entityGuids = request == null ? null : request.getEntityGuids(); + + if (classification == null || StringUtils.isEmpty(classification.getTypeName())) { + throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, "no classification"); + } + + if (CollectionUtils.isEmpty(entityGuids)) { + throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, "empty entity list"); + } + + final ITypedStruct trait = restAdapters.getTrait(classification); + + try { + metadataService.addTrait(entityGuids, trait); + } catch (IllegalArgumentException e) { + throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_NOT_FOUND, e); + } catch (AtlasException e) { + throw toAtlasBaseException(e); + } + } + private SearchFilter getSearchFilter() { SearchFilter searchFilter = new SearchFilter(); if (null != httpServletRequest && null != httpServletRequest.getParameterMap()) { diff --git a/webapp/src/test/java/org/apache/atlas/web/adapters/TestEntitiesREST.java b/webapp/src/test/java/org/apache/atlas/web/adapters/TestEntitiesREST.java index 90a46f8..c55234c 100644 --- a/webapp/src/test/java/org/apache/atlas/web/adapters/TestEntitiesREST.java +++ b/webapp/src/test/java/org/apache/atlas/web/adapters/TestEntitiesREST.java @@ -23,9 +23,11 @@ import org.apache.atlas.AtlasClient; import org.apache.atlas.RepositoryMetadataModule; import org.apache.atlas.RequestContext; import org.apache.atlas.TestUtilsV2; +import org.apache.atlas.model.instance.AtlasClassification; import org.apache.atlas.model.instance.AtlasEntity; import org.apache.atlas.model.instance.AtlasEntityHeader; import org.apache.atlas.model.instance.AtlasStruct; +import org.apache.atlas.model.instance.ClassificationAssociateRequest; import org.apache.atlas.model.instance.EntityMutationResponse; import org.apache.atlas.model.instance.EntityMutations; import org.apache.atlas.model.typedef.AtlasTypesDef; @@ -33,6 +35,7 @@ import org.apache.atlas.repository.graph.AtlasGraphProvider; import org.apache.atlas.store.AtlasTypeDefStore; import org.apache.atlas.web.rest.EntitiesREST; +import org.apache.atlas.web.rest.EntityREST; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; @@ -45,6 +48,7 @@ import org.testng.annotations.Test; import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,6 +63,9 @@ public class TestEntitiesREST { @Inject private EntitiesREST entitiesREST; + @Inject + private EntityREST entityREST; + private List<String> createdGuids = new ArrayList<>(); private AtlasEntity dbEntity; @@ -106,6 +113,18 @@ public class TestEntitiesREST { } } + @Test(dependsOnMethods = "testCreateOrUpdateEntities") + public void testTagToMultipleEntities() throws Exception{ + AtlasClassification tag = new AtlasClassification(TestUtilsV2.CLASSIFICATION, new HashMap<String, Object>() {{ put("tag", "tagName"); }}); + ClassificationAssociateRequest classificationAssociateRequest = new ClassificationAssociateRequest(createdGuids, tag); + entitiesREST.addClassification(classificationAssociateRequest); + for (String guid : createdGuids) { + final AtlasClassification result_tag = entityREST.getClassification(guid, TestUtilsV2.CLASSIFICATION); + Assert.assertNotNull(result_tag); + Assert.assertEquals(result_tag, tag); + } + } + @Test public void testUpdateWithSerializedEntities() throws Exception { //Check with serialization and deserialization of entity attributes for the case