Commit 765d556c by Sarath Subramanian Committed by Madhan Neethiraj

ATLAS-1564: EntityResource v1 updated to route its calls to v2 EntityREST

parent c7900f25
...@@ -270,12 +270,12 @@ public class AtlasClient extends AtlasBaseClient { ...@@ -270,12 +270,12 @@ public class AtlasClient extends AtlasBaseClient {
} }
public EntityResult(List<String> created, List<String> updated, List<String> deleted) { public EntityResult(List<String> created, List<String> updated, List<String> deleted) {
add(OP_CREATED, created); set(OP_CREATED, created);
add(OP_UPDATED, updated); set(OP_UPDATED, updated);
add(OP_DELETED, deleted); set(OP_DELETED, deleted);
} }
private void add(String type, List<String> list) { public void set(String type, List<String> list) {
if (list != null && list.size() > 0) { if (list != null && list.size() > 0) {
entities.put(type, list); entities.put(type, list);
} }
......
...@@ -66,6 +66,7 @@ public enum AtlasErrorCode { ...@@ -66,6 +66,7 @@ public enum AtlasErrorCode {
SYSTEM_TYPE(400, "ATLAS40035E", "{0} is a System-type"), SYSTEM_TYPE(400, "ATLAS40035E", "{0} is a System-type"),
INVALID_STRUCT_VALUE(400, "ATLAS40036E", "not a valid struct value {0}"), INVALID_STRUCT_VALUE(400, "ATLAS40036E", "not a valid struct value {0}"),
INSTANCE_LINEAGE_INVALID_PARAMS(400, "ATLAS40037E", "Invalid lineage query parameters passed {0}: {1}"), INSTANCE_LINEAGE_INVALID_PARAMS(400, "ATLAS40037E", "Invalid lineage query parameters passed {0}: {1}"),
ATTRIBUTE_UPDATE_NOT_SUPPORTED(400, "ATLAS40038E", "{0}.{1} : attribute update not supported"),
// All Not found enums go here // All Not found enums go here
TYPE_NAME_NOT_FOUND(404, "ATLAS4041E", "Given typename {0} was invalid"), TYPE_NAME_NOT_FOUND(404, "ATLAS4041E", "Given typename {0} was invalid"),
......
...@@ -34,7 +34,7 @@ public interface AtlasFormatConverter { ...@@ -34,7 +34,7 @@ public interface AtlasFormatConverter {
class ConverterContext { class ConverterContext {
private AtlasEntity.AtlasEntitiesWithExtInfo entities = null; private AtlasEntitiesWithExtInfo entities = null;
public void addEntity(AtlasEntity entity) { public void addEntity(AtlasEntity entity) {
if (entities == null) { if (entities == null) {
...@@ -61,6 +61,10 @@ public interface AtlasFormatConverter { ...@@ -61,6 +61,10 @@ public interface AtlasFormatConverter {
public boolean entityExists(String guid) { return entities != null && entities.hasEntity(guid); } public boolean entityExists(String guid) { return entities != null && entities.hasEntity(guid); }
public AtlasEntitiesWithExtInfo getEntities() { public AtlasEntitiesWithExtInfo getEntities() {
if (entities != null) {
entities.compact();
}
return entities; return entities;
} }
} }
......
...@@ -20,6 +20,7 @@ package org.apache.atlas.repository.converters; ...@@ -20,6 +20,7 @@ package org.apache.atlas.repository.converters;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.apache.atlas.AtlasClient; import org.apache.atlas.AtlasClient;
import org.apache.atlas.AtlasClient.EntityResult;
import org.apache.atlas.AtlasErrorCode; import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.AtlasException; import org.apache.atlas.AtlasException;
import org.apache.atlas.CreateUpdateEntitiesResult; import org.apache.atlas.CreateUpdateEntitiesResult;
...@@ -27,9 +28,11 @@ import org.apache.atlas.exception.AtlasBaseException; ...@@ -27,9 +28,11 @@ import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.TypeCategory; import org.apache.atlas.model.TypeCategory;
import org.apache.atlas.model.instance.AtlasClassification; import org.apache.atlas.model.instance.AtlasClassification;
import org.apache.atlas.model.instance.AtlasEntity; import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntityHeader; import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.EntityMutationResponse; import org.apache.atlas.model.instance.EntityMutationResponse;
import org.apache.atlas.model.instance.EntityMutations; import org.apache.atlas.model.instance.EntityMutations;
import org.apache.atlas.model.instance.EntityMutations.EntityOperation;
import org.apache.atlas.model.instance.GuidMapping; import org.apache.atlas.model.instance.GuidMapping;
import org.apache.atlas.services.MetadataService; import org.apache.atlas.services.MetadataService;
import org.apache.atlas.type.AtlasClassificationType; import org.apache.atlas.type.AtlasClassificationType;
...@@ -45,12 +48,17 @@ import org.apache.atlas.typesystem.Struct; ...@@ -45,12 +48,17 @@ import org.apache.atlas.typesystem.Struct;
import org.apache.atlas.typesystem.exception.EntityNotFoundException; import org.apache.atlas.typesystem.exception.EntityNotFoundException;
import org.apache.atlas.typesystem.exception.TraitNotFoundException; import org.apache.atlas.typesystem.exception.TraitNotFoundException;
import org.apache.atlas.typesystem.exception.TypeNotFoundException; import org.apache.atlas.typesystem.exception.TypeNotFoundException;
import org.apache.atlas.repository.converters.AtlasFormatConverter.ConverterContext;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
@Singleton @Singleton
public class AtlasInstanceConverter { public class AtlasInstanceConverter {
...@@ -100,10 +108,10 @@ public class AtlasInstanceConverter { ...@@ -100,10 +108,10 @@ public class AtlasInstanceConverter {
} }
} }
public Referenceable getReferenceable(AtlasEntity entity, final AtlasFormatConverter.ConverterContext ctx) throws AtlasBaseException { public Referenceable getReferenceable(AtlasEntity entity, final ConverterContext ctx) throws AtlasBaseException {
AtlasFormatConverter converter = instanceFormatters.getConverter(TypeCategory.ENTITY); AtlasFormatConverter converter = instanceFormatters.getConverter(TypeCategory.ENTITY);
AtlasType entityType = typeRegistry.getType(entity.getTypeName()); AtlasType entityType = typeRegistry.getType(entity.getTypeName());
Referenceable ref = (Referenceable)converter.fromV2ToV1(entity, entityType, ctx); Referenceable ref = (Referenceable) converter.fromV2ToV1(entity, entityType, ctx);
return ref; return ref;
} }
...@@ -111,7 +119,7 @@ public class AtlasInstanceConverter { ...@@ -111,7 +119,7 @@ public class AtlasInstanceConverter {
public ITypedStruct getTrait(AtlasClassification classification) throws AtlasBaseException { public ITypedStruct getTrait(AtlasClassification classification) throws AtlasBaseException {
AtlasFormatConverter converter = instanceFormatters.getConverter(TypeCategory.CLASSIFICATION); AtlasFormatConverter converter = instanceFormatters.getConverter(TypeCategory.CLASSIFICATION);
AtlasType classificationType = typeRegistry.getType(classification.getTypeName()); AtlasType classificationType = typeRegistry.getType(classification.getTypeName());
Struct trait = (Struct)converter.fromV2ToV1(classification, classificationType, new AtlasFormatConverter.ConverterContext()); Struct trait = (Struct)converter.fromV2ToV1(classification, classificationType, new ConverterContext());
try { try {
return metadataService.createTraitInstance(trait); return metadataService.createTraitInstance(trait);
...@@ -132,18 +140,17 @@ public class AtlasInstanceConverter { ...@@ -132,18 +140,17 @@ public class AtlasInstanceConverter {
return ret; return ret;
} }
public AtlasEntity.AtlasEntitiesWithExtInfo toAtlasEntity(IReferenceableInstance referenceable) throws AtlasBaseException { public AtlasEntitiesWithExtInfo toAtlasEntity(IReferenceableInstance referenceable) throws AtlasBaseException {
AtlasEntityFormatConverter converter = (AtlasEntityFormatConverter) instanceFormatters.getConverter(TypeCategory.ENTITY); AtlasEntityFormatConverter converter = (AtlasEntityFormatConverter) instanceFormatters.getConverter(TypeCategory.ENTITY);
AtlasEntityType entityType = typeRegistry.getEntityTypeByName(referenceable.getTypeName()); AtlasEntityType entityType = typeRegistry.getEntityTypeByName(referenceable.getTypeName());
if (entityType == null) { if (entityType == null) {
throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), referenceable.getTypeName()); throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), referenceable.getTypeName());
} }
AtlasFormatConverter.ConverterContext ctx = new AtlasFormatConverter.ConverterContext(); ConverterContext ctx = new ConverterContext();
AtlasEntity entity = converter.fromV1ToV2(referenceable, entityType, ctx);
AtlasEntity entity = converter.fromV1ToV2(referenceable, entityType, ctx);
ctx.addEntity(entity); ctx.addEntity(entity);
return ctx.getEntities(); return ctx.getEntities();
...@@ -212,6 +219,31 @@ public class AtlasInstanceConverter { ...@@ -212,6 +219,31 @@ public class AtlasInstanceConverter {
return context.getEntities(); return context.getEntities();
} }
public AtlasEntitiesWithExtInfo toAtlasEntities(String entitiesJson) throws AtlasBaseException, AtlasException {
ITypedReferenceableInstance[] referenceables = metadataService.deserializeClassInstances(entitiesJson);
AtlasEntityFormatConverter converter = (AtlasEntityFormatConverter) instanceFormatters.getConverter(TypeCategory.ENTITY);
ConverterContext context = new ConverterContext();
AtlasEntitiesWithExtInfo ret = null;
if (referenceables != null) {
for (IReferenceableInstance referenceable : referenceables) {
AtlasEntityType entityType = typeRegistry.getEntityTypeByName(referenceable.getTypeName());
if (entityType == null) {
throw new AtlasBaseException(AtlasErrorCode.TYPE_NAME_INVALID, TypeCategory.ENTITY.name(), referenceable.getTypeName());
}
AtlasEntity entity = converter.fromV1ToV2(referenceable, entityType, context);
context.addEntity(entity);
}
ret = context.getEntities();
}
return ret;
}
private AtlasEntity fromV1toV2Entity(Referenceable referenceable, AtlasFormatConverter.ConverterContext context) throws AtlasBaseException { private AtlasEntity fromV1toV2Entity(Referenceable referenceable, AtlasFormatConverter.ConverterContext context) throws AtlasBaseException {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("==> fromV1toV2Entity"); LOG.debug("==> fromV1toV2Entity");
...@@ -227,4 +259,69 @@ public class AtlasInstanceConverter { ...@@ -227,4 +259,69 @@ public class AtlasInstanceConverter {
return entity; return entity;
} }
public CreateUpdateEntitiesResult toCreateUpdateEntitiesResult(EntityMutationResponse reponse) {
CreateUpdateEntitiesResult ret = null;
if (reponse != null) {
Map<EntityOperation, List<AtlasEntityHeader>> mutatedEntities = reponse.getMutatedEntities();
Map<String, String> guidAssignments = reponse.getGuidAssignments();
ret = new CreateUpdateEntitiesResult();
if (MapUtils.isNotEmpty(guidAssignments)) {
ret.setGuidMapping(new GuidMapping(guidAssignments));
}
if (MapUtils.isNotEmpty(mutatedEntities)) {
EntityResult entityResult = new EntityResult();
for (Map.Entry<EntityOperation, List<AtlasEntityHeader>> e : mutatedEntities.entrySet()) {
switch (e.getKey()) {
case CREATE:
List<AtlasEntityHeader> createdEntities = mutatedEntities.get(EntityOperation.CREATE);
if (CollectionUtils.isNotEmpty(createdEntities)) {
entityResult.set(EntityResult.OP_CREATED, getGuids(createdEntities));
}
break;
case UPDATE:
List<AtlasEntityHeader> updatedEntities = mutatedEntities.get(EntityOperation.UPDATE);
if (CollectionUtils.isNotEmpty(updatedEntities)) {
entityResult.set(EntityResult.OP_UPDATED, getGuids(updatedEntities));
}
break;
case PARTIAL_UPDATE:
List<AtlasEntityHeader> partialUpdatedEntities = mutatedEntities.get(EntityOperation.PARTIAL_UPDATE);
if (CollectionUtils.isNotEmpty(partialUpdatedEntities)) {
entityResult.set(EntityResult.OP_UPDATED, getGuids(partialUpdatedEntities));
}
break;
case DELETE:
List<AtlasEntityHeader> deletedEntities = mutatedEntities.get(EntityOperation.DELETE);
if (CollectionUtils.isNotEmpty(deletedEntities)) {
entityResult.set(EntityResult.OP_DELETED, getGuids(deletedEntities));
}
break;
}
}
ret.setEntityResult(entityResult);
}
}
return ret;
}
public List<String> getGuids(List<AtlasEntityHeader> entities) {
List<String> ret = null;
if (CollectionUtils.isNotEmpty(entities)) {
ret = new ArrayList<>();
for (AtlasEntityHeader entity : entities) {
ret.add(entity.getGuid());
}
}
return ret;
}
} }
...@@ -89,6 +89,26 @@ public interface AtlasEntityStore { ...@@ -89,6 +89,26 @@ public interface AtlasEntityStore {
AtlasEntity entity) throws AtlasBaseException; AtlasEntity entity) throws AtlasBaseException;
/** /**
* Partial update a single entity using its guid.
* @param entityType type of the entity
* @param guid Entity guid
* @return EntityMutationResponse details of the updates performed by this call
* @throws AtlasBaseException
*
*/
EntityMutationResponse updateByGuid(AtlasEntityType entityType, String guid, AtlasEntity entity) throws AtlasBaseException;
/**
* Partial update entities attribute using its guid.
* @param guid Entity guid
* @param attrName attribute name to be updated
* @param attrValue updated attribute value
* @return EntityMutationResponse details of the updates performed by this call
* @throws AtlasBaseException
*/
EntityMutationResponse updateEntityAttributeByGuid(String guid, String attrName, Object attrValue) throws AtlasBaseException;
/**
* Delete an entity by its guid * Delete an entity by its guid
* @param guid * @param guid
* @return * @return
......
...@@ -21,15 +21,16 @@ package org.apache.atlas.repository.store.graph.v1; ...@@ -21,15 +21,16 @@ package org.apache.atlas.repository.store.graph.v1;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import org.apache.atlas.AtlasErrorCode; import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.AtlasException;
import org.apache.atlas.GraphTransaction; import org.apache.atlas.GraphTransaction;
import org.apache.atlas.RequestContextV1; import org.apache.atlas.RequestContextV1;
import org.apache.atlas.exception.AtlasBaseException; import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.impexp.AtlasImportResult; import org.apache.atlas.model.impexp.AtlasImportResult;
import org.apache.atlas.model.instance.AtlasClassification; import org.apache.atlas.model.instance.AtlasClassification;
import org.apache.atlas.model.instance.AtlasEntity; import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntityWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo; import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntityWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.AtlasObjectId; import org.apache.atlas.model.instance.AtlasObjectId;
import org.apache.atlas.model.instance.EntityMutationResponse; import org.apache.atlas.model.instance.EntityMutationResponse;
import org.apache.atlas.repository.graphdb.AtlasVertex; import org.apache.atlas.repository.graphdb.AtlasVertex;
...@@ -37,17 +38,28 @@ import org.apache.atlas.repository.store.graph.AtlasEntityStore; ...@@ -37,17 +38,28 @@ import org.apache.atlas.repository.store.graph.AtlasEntityStore;
import org.apache.atlas.repository.store.graph.EntityGraphDiscovery; import org.apache.atlas.repository.store.graph.EntityGraphDiscovery;
import org.apache.atlas.repository.store.graph.EntityGraphDiscoveryContext; import org.apache.atlas.repository.store.graph.EntityGraphDiscoveryContext;
import org.apache.atlas.type.AtlasEntityType; import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasStructType;
import org.apache.atlas.type.AtlasStructType.AtlasAttribute;
import org.apache.atlas.type.AtlasType;
import org.apache.atlas.type.AtlasTypeRegistry; import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.atlas.type.AtlasTypeUtil; import org.apache.atlas.type.AtlasTypeUtil;
import org.apache.atlas.typesystem.persistence.Id;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.*; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.apache.atlas.model.instance.EntityMutations.EntityOperation.*; import static org.apache.atlas.model.instance.EntityMutations.EntityOperation.DELETE;
import static org.apache.atlas.model.instance.EntityMutations.EntityOperation.UPDATE;
@Singleton @Singleton
...@@ -245,6 +257,65 @@ public class AtlasEntityStoreV1 implements AtlasEntityStore { ...@@ -245,6 +257,65 @@ public class AtlasEntityStoreV1 implements AtlasEntityStore {
return createOrUpdate(new AtlasEntityStream(updatedEntity), true); return createOrUpdate(new AtlasEntityStream(updatedEntity), true);
} }
@Override
@GraphTransaction
public EntityMutationResponse updateByGuid(AtlasEntityType entityType, String guid, AtlasEntity updatedEntity)
throws AtlasBaseException {
if (LOG.isDebugEnabled()) {
LOG.debug("==> updateByUniqueAttributes({}, {})", entityType.getTypeName(), guid);
}
if (updatedEntity == null) {
throw new AtlasBaseException(AtlasErrorCode.INVALID_PARAMETERS, "no entity to update.");
}
updatedEntity.setGuid(guid);
return createOrUpdate(new AtlasEntityStream(updatedEntity), true);
}
@Override
@GraphTransaction
public EntityMutationResponse updateEntityAttributeByGuid(String guid, String attrName, Object attrValue)
throws AtlasBaseException {
if (LOG.isDebugEnabled()) {
LOG.debug("==> updateEntityAttributeByGuid({}, {}, {})", guid, attrName, attrValue);
}
AtlasEntityWithExtInfo entityInfo = getById(guid);
if (entityInfo == null || entityInfo.getEntity() == null) {
throw new AtlasBaseException(AtlasErrorCode.INSTANCE_GUID_NOT_FOUND, guid);
}
AtlasEntity entity = entityInfo.getEntity();
AtlasEntityType entityType = (AtlasEntityType) typeRegistry.getType(entity.getTypeName());
AtlasAttribute attr = entityType.getAttribute(attrName);
if (attr == null) {
throw new AtlasBaseException(AtlasErrorCode.UNKNOWN_ATTRIBUTE, attrName, entity.getTypeName());
}
AtlasType attrType = attr.getAttributeType();
AtlasEntity updateEntity = new AtlasEntity();
updateEntity.setGuid(guid);
updateEntity.setTypeName(entity.getTypeName());
switch (attrType.getTypeCategory()) {
case PRIMITIVE:
case OBJECT_ID_TYPE:
updateEntity.setAttribute(attrName, attrValue);
break;
default:
throw new AtlasBaseException(AtlasErrorCode.ATTRIBUTE_UPDATE_NOT_SUPPORTED, attrName, attrType.getTypeName());
}
return createOrUpdate(new AtlasEntityStream(updateEntity), true);
}
@GraphTransaction @GraphTransaction
public EntityMutationResponse deleteById(final String guid) throws AtlasBaseException { public EntityMutationResponse deleteById(final String guid) throws AtlasBaseException {
......
...@@ -308,7 +308,8 @@ public class DefaultMetadataService implements MetadataService, ActiveStateChang ...@@ -308,7 +308,8 @@ public class DefaultMetadataService implements MetadataService, ActiveStateChang
return result; return result;
} }
private ITypedReferenceableInstance[] deserializeClassInstances(String entityInstanceDefinition) throws AtlasException { @Override
public ITypedReferenceableInstance[] deserializeClassInstances(String entityInstanceDefinition) throws AtlasException {
return GraphHelper.deserializeClassInstances(typeSystem, entityInstanceDefinition); return GraphHelper.deserializeClassInstances(typeSystem, entityInstanceDefinition);
} }
......
...@@ -301,4 +301,12 @@ public interface MetadataService { ...@@ -301,4 +301,12 @@ public interface MetadataService {
* @return * @return
*/ */
List<EntityAuditEvent> getAuditEvents(String guid, String startKey, short count) throws AtlasException; List<EntityAuditEvent> getAuditEvents(String guid, String startKey, short count) throws AtlasException;
/**
* Deserializes entity instances into ITypedReferenceableInstance array.
* @param entityInstanceDefinition
* @return ITypedReferenceableInstance[]
* @throws AtlasException
*/
ITypedReferenceableInstance[] deserializeClassInstances(String entityInstanceDefinition) throws AtlasException;
} }
...@@ -20,16 +20,28 @@ package org.apache.atlas.web.resources; ...@@ -20,16 +20,28 @@ package org.apache.atlas.web.resources;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.sun.jersey.api.core.ResourceContext;
import org.apache.atlas.AtlasClient; import org.apache.atlas.AtlasClient;
import org.apache.atlas.AtlasConstants; import org.apache.atlas.AtlasConstants;
import org.apache.atlas.AtlasException; import org.apache.atlas.AtlasException;
import org.apache.atlas.CreateUpdateEntitiesResult; import org.apache.atlas.CreateUpdateEntitiesResult;
import org.apache.atlas.EntityAuditEvent; import org.apache.atlas.EntityAuditEvent;
import org.apache.atlas.AtlasClient.EntityResult; import org.apache.atlas.AtlasClient.EntityResult;
import org.apache.atlas.exception.AtlasBaseException;
import org.apache.atlas.model.instance.AtlasEntity;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntityWithExtInfo;
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
import org.apache.atlas.model.instance.EntityMutationResponse;
import org.apache.atlas.model.instance.GuidMapping; import org.apache.atlas.model.instance.GuidMapping;
import org.apache.atlas.repository.converters.AtlasFormatConverter.ConverterContext;
import org.apache.atlas.repository.converters.AtlasInstanceConverter;
import org.apache.atlas.repository.store.graph.AtlasEntityStore;
import org.apache.atlas.services.MetadataService; import org.apache.atlas.services.MetadataService;
import org.apache.atlas.type.AtlasType; import org.apache.atlas.type.AtlasType;
import org.apache.atlas.type.AtlasEntityType;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.atlas.typesystem.IStruct; import org.apache.atlas.typesystem.IStruct;
import org.apache.atlas.typesystem.ITypedReferenceableInstance;
import org.apache.atlas.typesystem.Referenceable; import org.apache.atlas.typesystem.Referenceable;
import org.apache.atlas.typesystem.exception.EntityExistsException; import org.apache.atlas.typesystem.exception.EntityExistsException;
import org.apache.atlas.typesystem.exception.EntityNotFoundException; import org.apache.atlas.typesystem.exception.EntityNotFoundException;
...@@ -39,6 +51,7 @@ import org.apache.atlas.typesystem.json.InstanceSerialization; ...@@ -39,6 +51,7 @@ import org.apache.atlas.typesystem.json.InstanceSerialization;
import org.apache.atlas.typesystem.types.ValueConversionException; import org.apache.atlas.typesystem.types.ValueConversionException;
import org.apache.atlas.utils.AtlasPerfTracer; import org.apache.atlas.utils.AtlasPerfTracer;
import org.apache.atlas.utils.ParamChecker; import org.apache.atlas.utils.ParamChecker;
import org.apache.atlas.web.rest.EntityREST;
import org.apache.atlas.web.util.Servlets; import org.apache.atlas.web.util.Servlets;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.codehaus.jettison.json.JSONArray; import org.codehaus.jettison.json.JSONArray;
...@@ -59,7 +72,9 @@ import javax.ws.rs.core.UriInfo; ...@@ -59,7 +72,9 @@ import javax.ws.rs.core.UriInfo;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
...@@ -77,11 +92,17 @@ public class EntityResource { ...@@ -77,11 +92,17 @@ public class EntityResource {
private static final String TRAIT_NAME = "traitName"; private static final String TRAIT_NAME = "traitName";
private final MetadataService metadataService; private final MetadataService metadataService;
private final AtlasInstanceConverter restAdapters;
private final AtlasEntityStore entitiesStore;
private final AtlasTypeRegistry typeRegistry;
@Context @Context
UriInfo uriInfo; UriInfo uriInfo;
@Context
private ResourceContext resourceContext;
/** /**
* Created by the Guice ServletModule and injected with the * Created by the Guice ServletModule and injected with the
* configured MetadataService. * configured MetadataService.
...@@ -89,8 +110,11 @@ public class EntityResource { ...@@ -89,8 +110,11 @@ public class EntityResource {
* @param metadataService metadata service handle * @param metadataService metadata service handle
*/ */
@Inject @Inject
public EntityResource(MetadataService metadataService) { public EntityResource(MetadataService metadataService, AtlasInstanceConverter restAdapters, AtlasEntityStore entitiesStore, AtlasTypeRegistry typeRegistry) {
this.metadataService = metadataService; this.metadataService = metadataService;
this.restAdapters = restAdapters;
this.entitiesStore = entitiesStore;
this.typeRegistry = typeRegistry;
} }
/** /**
...@@ -131,15 +155,20 @@ public class EntityResource { ...@@ -131,15 +155,20 @@ public class EntityResource {
LOG.debug("submitting entities {} ", entityJson); LOG.debug("submitting entities {} ", entityJson);
} }
final CreateUpdateEntitiesResult result = metadataService.createEntities(entities); EntityREST entityREST = resourceContext.getResource(EntityREST.class);
final List<String> guids = result.getEntityResult().getCreatedEntities(); AtlasEntitiesWithExtInfo entitiesInfo = restAdapters.toAtlasEntities(entities);
EntityMutationResponse mutationResponse = entityREST.createOrUpdate(entitiesInfo);
final List<String> guids = restAdapters.getGuids(mutationResponse.getCreatedEntities());
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Created entities {}", guids); LOG.debug("Created entities {}", guids);
} }
JSONObject response = getResponse(result); final CreateUpdateEntitiesResult result = restAdapters.toCreateUpdateEntitiesResult(mutationResponse);
URI locationURI = getLocationURI(guids); JSONObject response = getResponse(result);
URI locationURI = getLocationURI(guids);
return Response.created(locationURI).entity(response).build(); return Response.created(locationURI).entity(response).build();
...@@ -165,7 +194,6 @@ public class EntityResource { ...@@ -165,7 +194,6 @@ public class EntityResource {
} }
} }
@VisibleForTesting @VisibleForTesting
public URI getLocationURI(List<String> guids) { public URI getLocationURI(List<String> guids) {
URI locationURI = null; URI locationURI = null;
...@@ -235,7 +263,10 @@ public class EntityResource { ...@@ -235,7 +263,10 @@ public class EntityResource {
LOG.info("updating entities {} ", entityJson); LOG.info("updating entities {} ", entityJson);
} }
CreateUpdateEntitiesResult result = metadataService.updateEntities(entities); EntityREST entityREST = resourceContext.getResource(EntityREST.class);
AtlasEntitiesWithExtInfo entitiesInfo = restAdapters.toAtlasEntities(entities);
EntityMutationResponse mutationResponse = entityREST.createOrUpdate(entitiesInfo);
CreateUpdateEntitiesResult result = restAdapters.toCreateUpdateEntitiesResult(mutationResponse);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Updated entities: {}", result.getEntityResult()); LOG.debug("Updated entities: {}", result.getEntityResult());
...@@ -317,11 +348,19 @@ public class EntityResource { ...@@ -317,11 +348,19 @@ public class EntityResource {
LOG.debug("Partially updating entity by unique attribute {} {} {} {} ", entityType, attribute, value, entityJson); LOG.debug("Partially updating entity by unique attribute {} {} {} {} ", entityType, attribute, value, entityJson);
} }
Referenceable updatedEntity = Referenceable updatedEntity = InstanceSerialization.fromJsonReferenceable(entityJson, true);
InstanceSerialization.fromJsonReferenceable(entityJson, true);
entityType = ParamChecker.notEmpty(entityType, "Entity type cannot be null");
attribute = ParamChecker.notEmpty(attribute, "attribute name cannot be null");
value = ParamChecker.notEmpty(value, "attribute value cannot be null");
Map<String, Object> attributes = new HashMap<>();
attributes.put(attribute, value);
CreateUpdateEntitiesResult result = AtlasEntitiesWithExtInfo entitiesInfo = restAdapters.toAtlasEntity(updatedEntity);
metadataService.updateEntityByUniqueAttribute(entityType, attribute, value, updatedEntity); AtlasEntity entity = entitiesInfo.getEntity(updatedEntity.getId()._getId());
EntityMutationResponse mutationResponse = entitiesStore.updateByUniqueAttributes(getEntityType(entityType), attributes, entity);
CreateUpdateEntitiesResult result = restAdapters.toCreateUpdateEntitiesResult(mutationResponse);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Updated entities: {}", result.getEntityResult()); LOG.debug("Updated entities: {}", result.getEntityResult());
...@@ -379,9 +418,9 @@ public class EntityResource { ...@@ -379,9 +418,9 @@ public class EntityResource {
} }
if (StringUtils.isEmpty(attribute)) { if (StringUtils.isEmpty(attribute)) {
return updateEntityPartialByGuid(guid, request); return partialUpdateEntityByGuid(guid, request);
} else { } else {
return updateEntityAttributeByGuid(guid, attribute, request); return partialUpdateEntityAttrByGuid(guid, attribute, request);
} }
} finally { } finally {
AtlasPerfTracer.log(perf); AtlasPerfTracer.log(perf);
...@@ -392,7 +431,7 @@ public class EntityResource { ...@@ -392,7 +431,7 @@ public class EntityResource {
} }
} }
private Response updateEntityPartialByGuid(String guid, HttpServletRequest request) { private Response partialUpdateEntityByGuid(String guid, HttpServletRequest request) {
String entityJson = null; String entityJson = null;
try { try {
guid = ParamChecker.notEmpty(guid, "Guid property cannot be null"); guid = ParamChecker.notEmpty(guid, "Guid property cannot be null");
...@@ -402,10 +441,11 @@ public class EntityResource { ...@@ -402,10 +441,11 @@ public class EntityResource {
LOG.debug("partially updating entity for guid {} : {} ", guid, entityJson); LOG.debug("partially updating entity for guid {} : {} ", guid, entityJson);
} }
Referenceable updatedEntity = Referenceable updatedEntity = InstanceSerialization.fromJsonReferenceable(entityJson, true);
InstanceSerialization.fromJsonReferenceable(entityJson, true); AtlasEntitiesWithExtInfo entitiesInfo = restAdapters.toAtlasEntity(updatedEntity);
AtlasEntity entity = entitiesInfo.getEntity(updatedEntity.getId()._getId());
CreateUpdateEntitiesResult result = metadataService.updateEntityPartialByGuid(guid, updatedEntity); EntityMutationResponse mutationResponse = entitiesStore.updateByGuid(getEntityType(updatedEntity.getTypeName()), guid, entity);
CreateUpdateEntitiesResult result = restAdapters.toCreateUpdateEntitiesResult(mutationResponse);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Updated entities: {}", result.getEntityResult()); LOG.debug("Updated entities: {}", result.getEntityResult());
...@@ -435,7 +475,7 @@ public class EntityResource { ...@@ -435,7 +475,7 @@ public class EntityResource {
* @postbody property's value * @postbody property's value
* @return response payload as json * @return response payload as json
*/ */
private Response updateEntityAttributeByGuid(String guid, String property, HttpServletRequest request) { private Response partialUpdateEntityAttrByGuid(String guid, String property, HttpServletRequest request) {
String value = null; String value = null;
try { try {
Preconditions.checkNotNull(property, "Entity property cannot be null"); Preconditions.checkNotNull(property, "Entity property cannot be null");
...@@ -446,7 +486,8 @@ public class EntityResource { ...@@ -446,7 +486,8 @@ public class EntityResource {
LOG.debug("Updating entity {} for property {} = {}", guid, property, value); LOG.debug("Updating entity {} for property {} = {}", guid, property, value);
} }
CreateUpdateEntitiesResult result = metadataService.updateEntityAttributeByGuid(guid, property, value); EntityMutationResponse mutationResponse = entitiesStore.updateEntityAttributeByGuid(guid, property, value);
CreateUpdateEntitiesResult result = restAdapters.toCreateUpdateEntitiesResult(mutationResponse);
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Updated entities: {}", result.getEntityResult()); LOG.debug("Updated entities: {}", result.getEntityResult());
...@@ -481,9 +522,9 @@ public class EntityResource { ...@@ -481,9 +522,9 @@ public class EntityResource {
@DELETE @DELETE
@Produces(Servlets.JSON_MEDIA_TYPE) @Produces(Servlets.JSON_MEDIA_TYPE)
public Response deleteEntities(@QueryParam("guid") List<String> guids, public Response deleteEntities(@QueryParam("guid") List<String> guids,
@QueryParam("type") String entityType, @QueryParam("type") String entityType,
@QueryParam("property") String attribute, @QueryParam("property") final String attribute,
@QueryParam("value") String value) { @QueryParam("value") final String value) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("==> EntityResource.deleteEntities({}, {}, {}, {})", guids, entityType, attribute, value); LOG.debug("==> EntityResource.deleteEntities({}, {}, {}, {})", guids, entityType, attribute, value);
} }
...@@ -494,19 +535,26 @@ public class EntityResource { ...@@ -494,19 +535,26 @@ public class EntityResource {
perf = AtlasPerfTracer.getPerfTracer(PERF_LOG, "EntityResource.deleteEntities(" + guids + ", " + entityType + ", " + attribute + ", " + value + ")"); perf = AtlasPerfTracer.getPerfTracer(PERF_LOG, "EntityResource.deleteEntities(" + guids + ", " + entityType + ", " + attribute + ", " + value + ")");
} }
AtlasClient.EntityResult entityResult; EntityResult entityResult;
EntityREST entityREST = resourceContext.getResource(EntityREST.class);
if (guids != null && !guids.isEmpty()) { if (guids != null && !guids.isEmpty()) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Deleting entities {}", guids); LOG.debug("Deleting entities {}", guids);
} }
entityResult = metadataService.deleteEntities(guids); EntityMutationResponse mutationResponse = entityREST.deleteByGuids(guids);
entityResult = restAdapters.toCreateUpdateEntitiesResult(mutationResponse).getEntityResult();
} else { } else {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Deleting entity type={} with property {}={}", entityType, attribute, value); LOG.debug("Deleting entity type={} with property {}={}", entityType, attribute, value);
} }
entityResult = metadataService.deleteEntityByUniqueAttribute(entityType, attribute, value); Map<String, Object> attributes = new HashMap<>();
attributes.put(attribute, value);
EntityMutationResponse mutationResponse = entitiesStore.deleteByUniqueAttributes(getEntityType(entityType), attributes);
entityResult = restAdapters.toCreateUpdateEntitiesResult(mutationResponse).getEntityResult();
} }
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
...@@ -634,7 +682,7 @@ public class EntityResource { ...@@ -634,7 +682,7 @@ public class EntityResource {
@Produces(Servlets.JSON_MEDIA_TYPE) @Produces(Servlets.JSON_MEDIA_TYPE)
public Response getEntity(@QueryParam("type") String entityType, public Response getEntity(@QueryParam("type") String entityType,
@QueryParam("property") String attribute, @QueryParam("property") String attribute,
@QueryParam("value") String value) { @QueryParam("value") final String value) {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("==> EntityResource.getEntity({}, {}, {})", entityType, attribute, value); LOG.debug("==> EntityResource.getEntity({}, {}, {})", entityType, attribute, value);
} }
...@@ -678,7 +726,26 @@ public class EntityResource { ...@@ -678,7 +726,26 @@ public class EntityResource {
attribute = ParamChecker.notEmpty(attribute, "attribute name cannot be null"); attribute = ParamChecker.notEmpty(attribute, "attribute name cannot be null");
value = ParamChecker.notEmpty(value, "attribute value cannot be null"); value = ParamChecker.notEmpty(value, "attribute value cannot be null");
final String entityDefinition = metadataService.getEntityDefinition(entityType, attribute, value); Map<String, Object> attributes = new HashMap<>();
attributes.put(attribute, value);
AtlasEntityWithExtInfo entityInfo;
try {
entityInfo = entitiesStore.getByUniqueAttributes(getEntityType(entityType), attributes);
} catch (AtlasBaseException e) {
LOG.error("Cannot find entity with type: {0}, attribute: {1} and value: {2}", entityType, attribute, value);
throw new WebApplicationException(Servlets.getErrorResponse(e, Response.Status.BAD_REQUEST));
}
String entityDefinition = null;
if (entityInfo != null) {
AtlasEntity entity = entityInfo.getEntity();
final ITypedReferenceableInstance instance = restAdapters.getITypedReferenceable(entity);
entityDefinition = InstanceSerialization.toJson(instance, true);
}
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
response.put(AtlasClient.REQUEST_ID, Servlets.getRequestId()); response.put(AtlasClient.REQUEST_ID, Servlets.getRequestId());
...@@ -694,10 +761,7 @@ public class EntityResource { ...@@ -694,10 +761,7 @@ public class EntityResource {
return Response.status(status).entity(response).build(); return Response.status(status).entity(response).build();
} catch (EntityNotFoundException e) { } catch (IllegalArgumentException e) {
LOG.error("An entity with type={} and qualifiedName={} does not exist", entityType, value, e);
throw new WebApplicationException(Servlets.getErrorResponse(e, Response.Status.NOT_FOUND));
} catch (AtlasException | IllegalArgumentException e) {
LOG.error("Bad type={}, qualifiedName={}", entityType, value, e); LOG.error("Bad type={}, qualifiedName={}", entityType, value, e);
throw new WebApplicationException(Servlets.getErrorResponse(e, Response.Status.BAD_REQUEST)); throw new WebApplicationException(Servlets.getErrorResponse(e, Response.Status.BAD_REQUEST));
} catch (Throwable e) { } catch (Throwable e) {
...@@ -1032,4 +1096,8 @@ public class EntityResource { ...@@ -1032,4 +1096,8 @@ public class EntityResource {
} }
return jsonArray; return jsonArray;
} }
private AtlasEntityType getEntityType(String typeName) {
return typeRegistry.getEntityTypeByName(typeName);
}
} }
...@@ -155,13 +155,30 @@ public class EntityREST { ...@@ -155,13 +155,30 @@ public class EntityREST {
return entitiesStore.updateByUniqueAttributes(entityType, uniqueAttributes, entity); return entitiesStore.updateByUniqueAttributes(entityType, uniqueAttributes, entity);
} }
/*******
* Entity Partial Update - Add/Update entity attribute identified by its GUID.
* Supports only uprimitive attribute type and entity references.
* does not support updation of complex types like arrays, maps
* Null updates are not possible
*******/
@PUT
@Consumes(Servlets.JSON_MEDIA_TYPE)
@Produces(Servlets.JSON_MEDIA_TYPE)
@Path("/guid/{guid}")
public EntityMutationResponse partialUpdateByGuid(@PathParam("guid") String guid,
@QueryParam("name") String attrName,
Object attrValue) throws Exception {
return entitiesStore.updateEntityAttributeByGuid(guid, attrName, attrValue);
}
/** /**
* Delete an entity identified by its GUID. * Delete an entity identified by its GUID.
* @param guid GUID for the entity * @param guid GUID for the entity
* @return EntityMutationResponse * @return EntityMutationResponse
*/ */
@DELETE @DELETE
@Path("guid/{guid}") @Path("/guid/{guid}")
@Consumes({Servlets.JSON_MEDIA_TYPE, MediaType.APPLICATION_JSON}) @Consumes({Servlets.JSON_MEDIA_TYPE, MediaType.APPLICATION_JSON})
@Produces(Servlets.JSON_MEDIA_TYPE) @Produces(Servlets.JSON_MEDIA_TYPE)
public EntityMutationResponse deleteByGuid(@PathParam("guid") final String guid) throws AtlasBaseException { public EntityMutationResponse deleteByGuid(@PathParam("guid") final String guid) throws AtlasBaseException {
......
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* * <p>
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* * <p>
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
...@@ -21,13 +21,22 @@ import static org.mockito.Mockito.never; ...@@ -21,13 +21,22 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import com.vividsolutions.jts.util.CollectionUtil;
import org.apache.atlas.AtlasClient.EntityResult; import org.apache.atlas.AtlasClient.EntityResult;
import org.apache.atlas.model.instance.AtlasEntityHeader;
import org.apache.atlas.model.instance.EntityMutationResponse;
import org.apache.atlas.model.instance.EntityMutations;
import org.apache.atlas.repository.converters.AtlasInstanceConverter;
import org.apache.atlas.repository.store.graph.AtlasEntityStore;
import org.apache.atlas.services.MetadataService; import org.apache.atlas.services.MetadataService;
import org.apache.atlas.type.AtlasTypeRegistry;
import org.apache.commons.collections.CollectionUtils;
import org.mockito.Matchers; import org.mockito.Matchers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
...@@ -42,8 +51,9 @@ public class EntityResourceTest { ...@@ -42,8 +51,9 @@ public class EntityResourceTest {
private static final String DELETED_GUID = "deleted_guid"; private static final String DELETED_GUID = "deleted_guid";
@Mock @Mock
MetadataService mockService; AtlasEntityStore entitiesStore;
@BeforeMethod @BeforeMethod
public void setUp() { public void setUp() {
...@@ -53,22 +63,35 @@ public class EntityResourceTest { ...@@ -53,22 +63,35 @@ public class EntityResourceTest {
@Test @Test
public void testDeleteEntitiesDoesNotLookupDeletedEntity() throws Exception { public void testDeleteEntitiesDoesNotLookupDeletedEntity() throws Exception {
List<String> guids = Collections.singletonList(DELETED_GUID); List<String> guids = Collections.singletonList(DELETED_GUID);
List<AtlasEntityHeader> deletedEntities = Collections.singletonList(new AtlasEntityHeader(null, DELETED_GUID, null));
// Create EntityResult with a deleted guid and no other guids. // Create EntityResult with a deleted guid and no other guids.
EntityResult entityResult = new EntityResult(Collections.<String>emptyList(), EntityMutationResponse resp = new EntityMutationResponse();
Collections.<String>emptyList(), guids); List<AtlasEntityHeader> headers = toAtlasEntityHeaders(guids);
when(mockService.deleteEntities(guids)).thenReturn(entityResult);
for (AtlasEntityHeader entity : headers) {
resp.addEntity(EntityMutations.EntityOperation.DELETE, entity);
}
when(entitiesStore.deleteByIds(guids)).thenReturn(resp);
// Create EntityResource with mock MetadataService. EntityMutationResponse response = entitiesStore.deleteByIds(guids);
EntityResource entityResource = new EntityResource(mockService);
List<AtlasEntityHeader> responseDeletedEntities = response.getDeletedEntities();
Assert.assertEquals(responseDeletedEntities, deletedEntities);
}
Response response = entityResource.deleteEntities(guids, null, null, null); private List<AtlasEntityHeader> toAtlasEntityHeaders(List<String> guids) {
List<AtlasEntityHeader> ret = null;
// Verify that if the EntityResult returned by MetadataService includes only deleted guids, if (CollectionUtils.isNotEmpty(guids)) {
// deleteEntities() does not perform any entity lookup. ret = new ArrayList<>(guids.size());
verify(mockService, never()).getEntityDefinition(Matchers.anyString()); for (String guid : guids) {
ret.add(new AtlasEntityHeader(null, guid, null));
}
}
EntityResult resultFromEntityResource = EntityResult.fromString(response.getEntity().toString()); return ret;
Assert.assertTrue(resultFromEntityResource.getDeletedEntities().contains(DELETED_GUID));
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment