package org.ovirt.engine.core.bll; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.lang.StringUtils; import org.ovirt.engine.core.bll.interfaces.BackendInternal; import org.ovirt.engine.core.common.BackendService; import org.ovirt.engine.core.common.action.TagsOperationParameters; import org.ovirt.engine.core.common.action.VdcActionType; import org.ovirt.engine.core.common.businessentities.Tags; import org.ovirt.engine.core.common.interfaces.ITagsHandler; import org.ovirt.engine.core.compat.Guid; import org.ovirt.engine.core.dao.TagDao; import org.ovirt.engine.core.utils.collections.CopyOnAccessMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class responsible to In memory Tags handling. On Vdc starting in memory tags tree initialized. All Tags changing * operations go throw this class */ @Singleton public class TagsDirector implements BackendService, ITagsHandler { /** * This pattern is used to replace '\\' in the expression that may be added by handling a '_' character with an * empty string. Since we use both String and RegExp , each backslash char is represented by four backslash * characters, so for marching two backslashes we will need eight. */ private static final Pattern BACKSLASH_REMOVER = Pattern.compile("\\\\\\\\"); private enum TagReturnValueIndicator { ID, NAME } private static final Logger log = LoggerFactory.getLogger(TagsDirector.class); protected static final Guid ROOT_TAG_ID = Guid.Empty; @Inject private BackendInternal backend; @Inject private TagDao tagDao; /** * In memory nodes cache for quicker access to each node by ID: O(1) instead O(lnN) of tree */ private final Map<Guid, Tags> tagsMapByID = new CopyOnAccessMap<>(new HashMap<>()); /** * In memory nodes cache for quicker access to each node by name */ private final Map<String, Tags> tagsMapByName = new CopyOnAccessMap<>(new HashMap<>()); /** * In memory tree initialized during initialization */ @PostConstruct protected void init() { log.info("Start initializing {}", getClass().getSimpleName()); tagsMapByID.clear(); tagsMapByName.clear(); Tags root = new Tags("root", null, true, ROOT_TAG_ID, "root"); addTagToHash(root); addChildren(root); log.info("Finished initializing {}", getClass().getSimpleName()); } private void addTagToHash(Tags tag) { tagsMapByID.put(tag.getTagId(), tag); tagsMapByName.put(tag.getTagName(), tag); if (tag.getParentId() != null) { // If the tag has a parent, the parent should have in its children the added tag instead // of the old version of the tag , if exists Tags parentTag = tagsMapByID.get(tag.getParentId()); if (parentTag == null) { log.error("Could not obtain tag for guid '{}'", tag.getParentId()); return; } List<Tags> parentChildren = parentTag.getChildren(); replaceTagInChildren(tag, parentChildren); addTagToHash(parentTag); // replace the parent tag after the modification } } private static void replaceTagInChildren(Tags tag, List<Tags> parentChildren) { for (int counter = 0; counter < parentChildren.size(); counter++) { if (parentChildren.get(counter).getTagId().equals(tag.getTagId())) { parentChildren.set(counter, tag); break; } } } private void removeTagFromHash(Tags tag) { tagsMapByID.remove(tag.getTagId()); tagsMapByName.remove(tag.getTagName()); } /** * Recursive tree initialization call */ private void addChildren(Tags tag) { log.info("Tag '{}' added to tree", tag.getTagName()); List<Tags> children = tagDao.getAllForParent(tag.getTagId()); for (Tags child : children) { addChildren(child); log.info("Tag '{}' added as child to parent '{}'", child.getTagName(), tag.getTagName()); tag.getChildren().add(child); addTagToHash(tag); addTagToHash(child); } } private void removeTagAndChildren(Tags tag) { for (Tags child : tag.getChildren()) { removeTagAndChildren(child); } removeTagFromHash(tag); } public void addTag(Tags tag) { if (tagsMapByID.containsKey(tag.getParentId())) { Tags parent = tagsMapByID.get(tag.getParentId()); parent.getChildren().add(tag); addTagToHash(tag); addTagToHash(parent); } else { log.error("Trying to add tag '{}', parent doesn't exist in Data Structure - '{}'", tag.getTagName(), tag.getParentId()); } } /** * Remove tag operation. For tag with children all tag's children will be removed as well * * @param tagId * tag to remove */ public void removeTag(Guid tagId) { if (tagsMapByID.containsKey(tagId)) { Tags tag = tagsMapByID.get(tagId); removeTagAndChildren(tag); Tags parent = tagsMapByID.get(tag.getParentId()); parent.getChildren().remove(tag); addTagToHash(parent); } else { log.warn("Trying to remove tag, not exists in Data Structure - '{}'", tagId); } } /** * Update tag. We assume that the id doesn't change. */ public void updateTag(Tags tag) { if (tagsMapByID.containsKey(tag.getTagId())) { Tags tagFromCache = tagsMapByID.get(tag.getTagId()); String oldName = tagFromCache.getTagName(); // check if tag name has changed. If it has - modify name dictionary // accordingly: if (!tag.getTagName().equals(oldName)) { tagsMapByName.remove(oldName); } // Copy the children of the cached tag to keep the object hierarchy consistent. tag.setChildren(tagFromCache.getChildren()); addTagToHash(tag); } else { log.warn("Trying to update tag, not exists in Data Structure - '{}'", tag.getTagName()); } } public void moveTag(Guid tagId, Guid newParent) { if (tagsMapByID.containsKey(tagId)) { Tags tag = tagsMapByID.get(tagId); if (tagsMapByID.containsKey(newParent)) { if (tagsMapByID.containsKey(tag.getParentId())) { Tags parentTag = tagsMapByID.get(tag.getParentId()); parentTag.getChildren().remove(tag); addTagToHash(parentTag); } else { log.warn("Trying to move tag from parent that doesn't exist in Data Structure - '{}'", tag.getParentId()); } Tags newParentTag = tagsMapByID.get(newParent); newParentTag.getChildren().add(tag); tag.setParentId(newParent); addTagToHash(newParentTag); // Parent got changed, modify it. updateTagInBackend(tag); } else { log.error("Trying to move tag, to parent not exists in Data Structure - '{}'", newParent); } } else { log.error("Trying to move tag, not exists in Data Structure - '{}'", tagId); } } protected void updateTagInBackend(Tags tag) { backend.runInternalAction(VdcActionType.UpdateTag, new TagsOperationParameters(tag)); } /** * This function will return the tag's ID and its children IDs. Its used to determine if a tag is assigned to an * entity. Tag is determined as assigned to an entity if the entity is assigned to the tag or to one of its * children. * * @param tagId * the ID of the 'root' tag. * @return a comma separated list of IDs. */ @Override public String getTagIdAndChildrenIds(Guid tagId) { Tags tag = getTagById(tagId); if (tag == null) { return StringUtils.EMPTY; } StringBuilder sb = tag.getTagIdAndChildrenIds(); return sb.toString(); } @Override public String getTagNameAndChildrenNames(Guid tagId) { Tags tag = getTagById(tagId); StringBuilder sb = tag.getTagNameAndChildrenNames(); return sb.toString(); } public HashSet<Guid> getTagIdAndChildrenIdsAsSet(Guid tagId) { Tags tag = getTagById(tagId); HashSet<Guid> set = new HashSet<>(); tag.getTagIdAndChildrenIdsAsList(set); return set; } /** * This function will return the tag's ID and its children IDs. Its used to determine if a tag is assigned to an * entity. Tag is determined as assigned to an entity if the entity is assigned to the tag or to one of its * children. * * @param tagName * the name of the 'root' tag. * @return a comma separated list of IDs. */ public String getTagIdAndChildrenIds(String tagName) { Tags tag = getTagByTagName(tagName); StringBuilder sb = tag.getTagIdAndChildrenIds(); return sb.toString(); } @Override public String getTagNamesAndChildrenNamesByRegExp(String tagNameRegExp) { // add RegEx chars or beginning of string ('^') and end of string ('$'): tagNameRegExp = String.format("^%1$s$", tagNameRegExp); // convert to the regular expression format: tagNameRegExp = tagNameRegExp.replace("*", ".*"); StringBuilder sb = new StringBuilder(); recursiveGetTagsAndChildrenByRegExp(tagNameRegExp, sb, getRootTag(), TagReturnValueIndicator.NAME); return sb.toString(); } private static void recursiveGetTagsAndChildrenByRegExp(String tagNameRegExp, StringBuilder sb, Tags tag, TagReturnValueIndicator indicator) { if ((tag.getChildren() != null) && !tag.getChildren().isEmpty()) { tagNameRegExp = BACKSLASH_REMOVER.matcher(tagNameRegExp).replaceAll(""); for (Tags child : tag.getChildren()) { Pattern tagNamePattern = Pattern.compile(tagNameRegExp); if (tagNamePattern.matcher(child.getTagName()).find()) { // the tag matches the regular expression -> add it and all its // children // (we prevent searching a regular expression match on them - // unnecessary). if (sb.length() == 0) { if (indicator == TagReturnValueIndicator.ID) { sb.append(child.getTagIdAndChildrenIds()); } else { sb.append(child.getTagNameAndChildrenNames()); } } else { if (indicator == TagReturnValueIndicator.ID) { sb.append(String.format(",%1$s", child.getTagIdAndChildrenIds())); } else { sb.append(String.format(",%1$s", child.getTagNameAndChildrenNames())); } } } else { recursiveGetTagsAndChildrenByRegExp(tagNameRegExp, sb, child, indicator); } } } } /** * Get tag from in memory data structure (by ID). This tag will be with all children tree initialized as opposite to * tag from db. */ public Tags getTagById(Guid tagId) { if (tagsMapByID.containsKey(tagId)) { return tagsMapByID.get(tagId); } else { return null; } } /** * Get tag from in memory data structure (by name). */ @Override public Tags getTagByTagName(String tagName) { if (tagsMapByName.containsKey(tagName)) { return tagsMapByName.get(tagName); } else { return null; } } /** * Gets a list of all the tags in the system. * * @return a tags list. */ public ArrayList<Tags> getAllTags() { ArrayList<Tags> ret = new ArrayList<>(tagsMapByID.values()); // remove the root - it is not a real tag: ret.remove(getRootTag()); return ret; } /** * Returns the root tag in the system. * * @return the root tag. */ public Tags getRootTag() { return tagsMapByID.get(ROOT_TAG_ID); } public boolean isTagDescestorOfTag(Guid sourceTagId, Guid potentialDescestorId) { if (sourceTagId.equals(potentialDescestorId)) { return true; } Tags tag = getTagById(sourceTagId); if (tag != null && tag.getChildren() != null) { for (Tags childTag : tag.getChildren()) { if (isTagDescestorOfTag(childTag.getTagId(), potentialDescestorId)) { return true; } } } return false; } }