/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.model.reference;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.xwiki.model.EntityType;
/**
* A set of entities references.
*
* @version $Id: 0c462c78d7420c277617c7e165c2a423e62abfec $
* @since 5.2M2
*/
public class EntityReferenceSet
{
private static class EntityReferenceEntryChildren
{
public EntityType childrenType;
public Map<String, EntityReferenceEntry> children;
public EntityReferenceEntry add(String name, Map<String, Serializable> entityParameters)
{
if (this.children == null) {
this.children = new HashMap<String, EntityReferenceEntry>();
}
EntityReferenceEntry entry = this.children.get(name);
if (entry == null) {
entry = new EntityReferenceEntry(this.childrenType, name);
if (entityParameters != null) {
entry.addParameters(entityParameters);
}
this.children.put(name, entry);
} else {
if (!entry.matches(entityParameters)) {
entry.addParameters(entityParameters);
}
}
return entry;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append(this.childrenType);
builder.append('(');
builder.append(this.children);
return builder.toString();
}
}
private static class EntityReferenceEntry
{
public EntityType type;
public String name;
public List<Map<String, Serializable>> parameters;
public Map<EntityType, EntityReferenceEntryChildren> children;
public EntityReferenceEntry()
{
}
public EntityReferenceEntry(EntityType type, String name)
{
this.type = type;
this.name = name;
}
public EntityReferenceEntry add()
{
return add(null, null, null);
}
public EntityReferenceEntryChildren getChildren(EntityType entityType, boolean create)
{
if (this.children == null) {
if (!create) {
return null;
}
this.children = new HashMap<>();
}
EntityReferenceEntryChildren child = this.children.get(entityType);
if (child == null) {
if (!create) {
return null;
}
child = new EntityReferenceEntryChildren();
child.childrenType = entityType;
this.children.put(entityType, child);
}
return child;
}
public EntityReferenceEntryChildren getClosestChildren(EntityType entityType)
{
EntityReferenceEntryChildren typedChildren = getChildren(entityType, false);
if (typedChildren != null) {
return typedChildren;
}
// Search for the closest
if (this.children == null) {
return null;
}
for (Map.Entry<EntityType, EntityReferenceEntryChildren> entry : this.children.entrySet()) {
EntityReferenceEntryChildren typedChilrendEntry = entry.getValue();
if (typedChilrendEntry.childrenType.ordinal() > entityType.ordinal()) {
// Only return a potential child of the passed type
if (typedChildren == null
|| typedChildren.childrenType.ordinal() > typedChilrendEntry.childrenType.ordinal()) {
typedChildren = typedChilrendEntry;
}
}
}
return typedChildren;
}
public EntityReferenceEntry add(EntityType entityType, String entityName,
Map<String, Serializable> entityParameters)
{
EntityReferenceEntryChildren child = getChildren(entityType, true);
return child.add(entityName, entityParameters);
}
private void addParameters(Map<String, Serializable> entityParameters)
{
if (!entityParameters.isEmpty()) {
if (this.parameters == null) {
this.parameters = new ArrayList<Map<String, Serializable>>();
}
this.parameters.add(entityParameters);
}
}
public boolean matches(EntityReference reference)
{
return matches(reference.getParameters());
}
public boolean matches(Map<String, Serializable> referenceParameters)
{
if (parameters == null) {
return true;
}
boolean matches = parameters.isEmpty();
for (Map<String, Serializable> parametersMap : parameters) {
matches |= matches(referenceParameters, parametersMap);
}
return matches;
}
private boolean matches(Map<String, Serializable> referenceParameters, Map<String, Serializable> map)
{
for (Map.Entry<String, Serializable> entry : map.entrySet()) {
if (referenceParameters.containsKey(entry.getKey())) {
if (!Objects.equals(entry.getValue(), referenceParameters.get(entry.getKey()))) {
return false;
}
}
}
return true;
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
if (this.type != null) {
builder.append(this.type);
builder.append(':');
builder.append(this.name);
}
if (this.children != null) {
builder.append('(');
builder.append(this.children);
builder.append(')');
}
return builder.toString();
}
}
private EntityReferenceEntry includes;
private EntityReferenceEntry excludes;
private void add(EntityReference reference, EntityReferenceEntry entry)
{
EntityReferenceEntry currentEntry = entry;
for (EntityReference element : reference.getReversedReferenceChain()) {
// Move the first element to the right level in the tree
if (currentEntry == entry) {
while (currentEntry.children != null && currentEntry.getClosestChildren(element.getType()) == null) {
currentEntry = currentEntry.add();
}
}
currentEntry = currentEntry.add(element.getType(), element.getName(), element.getParameters());
}
}
/**
* @param reference the reference
* @return the entity reference set
*/
public EntityReferenceSet includes(EntityReference reference)
{
if (this.includes == null) {
this.includes = new EntityReferenceEntry();
}
add(reference, this.includes);
return this;
}
/**
* @param reference the reference
* @return the entity reference set
*/
public EntityReferenceSet excludes(EntityReference reference)
{
if (this.excludes == null) {
this.excludes = new EntityReferenceEntry();
}
add(reference, this.excludes);
return this;
}
private boolean matchesInclude(EntityReference reference)
{
if (this.includes == null) {
return true;
}
EntityReferenceEntry currentEntry = this.includes;
for (EntityReference element : reference.getReversedReferenceChain()) {
// No children
if (currentEntry.children == null) {
return true;
}
EntityReferenceEntryChildren typedChildren = currentEntry.children.get(element.getType());
if (typedChildren != null) {
// Children have same type
EntityReferenceEntry nameEntry = typedChildren.children.get(element.getName());
if (nameEntry == null) {
currentEntry = typedChildren.children.get(null);
if (currentEntry == null) {
return false;
}
} else {
currentEntry = nameEntry;
}
if (!currentEntry.matches(element)) {
return false;
}
} else {
if (currentEntry.name != null || currentEntry.getClosestChildren(element.getType()) == null) {
return false;
}
}
}
return true;
}
private boolean matchesExclude(EntityReference reference)
{
if (this.excludes == null) {
return true;
}
EntityReferenceEntry currentEntry = this.excludes;
for (EntityReference element : reference.getReversedReferenceChain()) {
// No children
if (currentEntry.children == null) {
return false;
}
EntityReferenceEntryChildren typedChildren = currentEntry.children.get(element.getType());
if (typedChildren != null) {
// Children have same type
EntityReferenceEntry nameEntry = typedChildren.children.get(element.getName());
if (nameEntry == null) {
currentEntry = typedChildren.children.get(null);
if (currentEntry == null) {
return true;
}
} else {
currentEntry = nameEntry;
}
if (!currentEntry.matches(element)) {
return true;
}
}
}
// Check if the reference is shorter than the exclude(s)
return currentEntry.children != null
// Check if the excluded parameters are the same as the one the reference contains
|| (currentEntry.parameters != null && CollectionUtils.intersection(
getParametersKeys(currentEntry.parameters), reference.getParameters().keySet()).isEmpty());
}
private Set<String> getParametersKeys(List<Map<String, Serializable>> parameters)
{
if (parameters.isEmpty()) {
return Collections.emptySet();
}
Set<String> set = new HashSet<String>();
for (Map<String, Serializable> map : parameters) {
set.addAll(map.keySet());
}
return set;
}
/**
* @param reference the reference
* @return the entity reference set
*/
public boolean matches(EntityReference reference)
{
return matchesInclude(reference) && matchesExclude(reference);
}
}