/**
* Copyright 2016 Hortonworks.
*
* 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 com.hortonworks.registries.tag.service;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.hortonworks.registries.common.QueryParam;
import com.hortonworks.registries.storage.Storable;
import com.hortonworks.registries.storage.StorableKey;
import com.hortonworks.registries.storage.StorageManager;
import com.hortonworks.registries.storage.util.StorageUtils;
import com.hortonworks.registries.tag.Tag;
import com.hortonworks.registries.tag.TaggedEntity;
import com.hortonworks.registries.tag.TagStorableMapping;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Catalog db based tag service.
*/
public class CatalogTagService implements TagService {
private static final String TAG_NAMESPACE = new Tag().getNameSpace();
private static final String TAG_STORABLE_MAPPING_NAMESPACE = new TagStorableMapping().getNameSpace();
private final StorageManager dao;
public CatalogTagService(StorageManager dao) {
this.dao = dao;
dao.registerStorables(getStorableClasses());
}
public static Collection<Class<? extends Storable>> getStorableClasses() {
InputStream resourceAsStream = CatalogTagService.class.getClassLoader().getResourceAsStream("tagstorables.props");
HashSet<Class<? extends Storable>> classes = new HashSet<>();
try {
List<String> classNames = IOUtils.readLines(resourceAsStream);
for (String className : classNames) {
classes.add((Class<? extends Storable>) Class.forName(className));
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
return classes;
}
// handle this check at application layer since in-memory storage etc does not contain unique key constraint
private void validateTag(Tag tag) {
StorageUtils.ensureUnique(tag, this::listTags, QueryParam.params("name", tag.getName()));
}
@Override
public Tag addTag(Tag tag) {
if (tag.getId() == null) {
tag.setId(dao.nextId(TAG_NAMESPACE));
}
if (tag.getTimestamp() == null) {
tag.setTimestamp(System.currentTimeMillis());
}
validateTag(tag);
checkCycles(tag, tag.getTags());
dao.add(tag);
addTagsForStorable(getTaggedEntity(tag), tag.getTags());
return tag;
}
private void checkCycles(Tag current, List<Tag> tags) {
for (Tag tag : tags) {
if (tag.equals(current) || flatten(getTags(getTaggedEntity(tag))).contains(current)) {
throw new IllegalArgumentException("Tagging " + current +
" with " + tag + " would result in a cycle.");
}
}
}
private TaggedEntity getTaggedEntity(Tag tag) {
return new TaggedEntity(tag.getNameSpace(), tag.getId());
}
private List<Tag> flatten(Tag tag) {
return flatten(Collections.singletonList(tag));
}
private List<Tag> flatten(List<Tag> tags) {
List<Tag> res = new ArrayList<>();
for (Tag tag: tags) {
res.add(tag);
res.addAll(flatten(tag.getTags()));
}
return res;
}
@Override
public Tag addOrUpdateTag(Long tagId, Tag tag) {
if (tag.getId() == null) {
tag.setId(tagId);
}
if (tag.getTimestamp() == null) {
tag.setTimestamp(System.currentTimeMillis());
}
validateTag(tag);
List<Tag> existingTags = getTags(getTaggedEntity(tag));
List<Tag> tagsToBeAdded = getTagsToBeAdded(existingTags, tag.getTags());
List<Tag> tagsToBeRemoved = getTagsToBeRemoved(existingTags, tag.getTags());
checkCycles(tag, tagsToBeAdded);
this.dao.addOrUpdate(tag);
updateTags(getTaggedEntity(tag), tagsToBeAdded, tagsToBeRemoved);
return tag;
}
@Override
public Tag getTag(Long tagId) {
Tag tag = new Tag();
tag.setId(tagId);
Tag result = this.dao.get(new StorableKey(TAG_NAMESPACE, tag.getPrimaryKey()));
if (result != null) {
result.setTags(getTags(getTaggedEntity(result)));
}
return result;
}
@Override
public Tag removeTag(Long tagId) {
Tag tag = getTag(tagId);
if (tag != null) {
if (!getEntities(tagId, false).isEmpty()) {
throw new TagNotEmptyException("Tag not empty, has child entities.");
}
removeTagsFromStorable(getTaggedEntity(tag), tag.getTags());
dao.<Tag>remove(new StorableKey(TAG_NAMESPACE, tag.getPrimaryKey()));
}
return tag;
}
@Override
public Collection<Tag> listTags() {
return makeTags(this.dao.<Tag>list(TAG_NAMESPACE));
}
@Override
public Collection<Tag> listTags(List<QueryParam> queryParams) {
return makeTags(dao.<Tag>find(TAG_NAMESPACE, queryParams));
}
@Override
public void addTagsForStorable(TaggedEntity taggedEntity, List<Tag> tags) {
if (tags != null) {
for (Tag tag : tags) {
TagStorableMapping tagStorable = new TagStorableMapping();
tagStorable.setTagId(tag.getId());
tagStorable.setStorableNamespace(taggedEntity.getNamespace());
tagStorable.setStorableId(taggedEntity.getId());
this.dao.add(tagStorable);
}
}
}
@Override
public void addOrUpdateTagsForStorable(TaggedEntity taggedEntity, List<Tag> tags) {
List<Tag> existingTags = getTags(taggedEntity);
updateTags(taggedEntity, getTagsToBeAdded(existingTags, tags), getTagsToBeRemoved(existingTags, tags));
}
private List<Tag> getTagsToBeRemoved(List<Tag> existing, List<Tag> newList) {
return Lists.newArrayList(
Sets.difference(ImmutableSet.copyOf(existing), ImmutableSet.copyOf(newList)));
}
private List<Tag> getTagsToBeAdded(List<Tag> existing, List<Tag> newList) {
return Lists.newArrayList(
Sets.difference(ImmutableSet.copyOf(newList), ImmutableSet.copyOf(existing)));
}
private void updateTags(TaggedEntity taggedEntity, List<Tag> tagsToBeAdded, List<Tag> tagsToBeRemoved) {
removeTagsFromStorable(taggedEntity, tagsToBeRemoved);
addTagsForStorable(taggedEntity, tagsToBeAdded);
}
@Override
public void removeTagsFromStorable(TaggedEntity taggedEntity, List<Tag> tags) {
if (tags != null) {
for (Tag tag : tags) {
TagStorableMapping tagStorable = new TagStorableMapping();
tagStorable.setTagId(tag.getId());
tagStorable.setStorableId(taggedEntity.getId());
tagStorable.setStorableNamespace(taggedEntity.getNamespace());
this.dao.remove(tagStorable.getStorableKey());
}
}
}
@Override
public List<Tag> getTags(TaggedEntity taggedEntity) {
List<Tag> tags = new ArrayList<>();
QueryParam qp1 = new QueryParam(TagStorableMapping.FIELD_STORABLE_ID,
String.valueOf(taggedEntity.getId()));
QueryParam qp2 = new QueryParam(TagStorableMapping.FIELD_STORABLE_NAMESPACE,
String.valueOf(taggedEntity.getNamespace()));
for (TagStorableMapping mapping : listTagStorableMapping(ImmutableList.of(qp1, qp2))) {
tags.add(getTag(mapping.getTagId()));
}
return tags;
}
enum State {
VISITING, VISITED
}
@Override
public List<TaggedEntity> getEntities(Long tagId, boolean recurse) {
return getEntities(tagId, recurse, new HashMap<Long, State>());
}
public List<TaggedEntity> getEntities(Long tagId, boolean recurse, Map<Long, State> state) {
State tagState = state.get(tagId);
Set<TaggedEntity> result = new HashSet<>();
if (tagState == State.VISITING) {
throw new IllegalStateException("Cycle detected");
} else if (tagState != State.VISITED) {
state.put(tagId, State.VISITING);
for (TaggedEntity taggedEntity : getTaggedEntities(tagId)) {
if (recurse && Tag.NAMESPACE.equalsIgnoreCase(taggedEntity.getNamespace())) {
result.addAll(getEntities(taggedEntity.getId(), recurse, state));
} else {
result.add(taggedEntity);
}
}
state.put(tagId, State.VISITED);
}
return new LinkedList<>(result);
}
private List<TaggedEntity> getTaggedEntities(Long tagId) {
List<TaggedEntity> taggedEntities = new ArrayList<>();
QueryParam qp1 = new QueryParam(TagStorableMapping.FIELD_TAG_ID, String.valueOf(tagId));
for (TagStorableMapping mapping : listTagStorableMapping(ImmutableList.of(qp1))) {
taggedEntities.add(new TaggedEntity(mapping.getStorableNamespace(), mapping.getStorableId()));
}
return taggedEntities;
}
private Collection<TagStorableMapping> listTagStorableMapping(List<QueryParam> params) {
return dao.find(TAG_STORABLE_MAPPING_NAMESPACE, params);
}
private Collection<Tag> makeTags(Collection<Tag> tags) {
if (tags != null) {
for (Tag tag : tags) {
tag.setTags(getTags(getTaggedEntity(tag)));
}
}
return tags;
}
}