/*
* 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.internal.reference;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.xwiki.component.phase.Initializable;
import org.xwiki.model.EntityType;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.EntityReferenceResolver;
/**
* Generic implementation deferring default values for unspecified reference parts to extending classes. This allows for
* example both the Current Entity Reference Resolver and the Default Entity Reference Resolver to share the code from
* this class.
*
* @see AbstractEntityReferenceResolver
* @version $Id: 3edf4ffaf005810377ed6b87173fc9552d304826 $
* @since 2.2M1
*/
public abstract class AbstractStringEntityReferenceResolver extends AbstractEntityReferenceResolver implements
EntityReferenceResolver<String>, Initializable
{
/**
* Array of character to unescape in entity names.
*/
private String[] escapeMatching;
/**
* The replacement array corresponding to the array in {@link #escapeMatching} array.
*/
private String[] escapeMatchingReplace;
@Inject
private SymbolScheme symbolScheme;
private Map<EntityType, Map<Character, EntityType>> referenceSetup;
/**
* Empty constructor, to be used by the Component Manager, which will also inject the Symbol Scheme.
*/
public AbstractStringEntityReferenceResolver()
{
// Empty constructor, to be used by the Component Manager, which will also inject the Symbol Scheme
}
/**
* Constructor to be used when using this class as a POJO and not as a component.
*
* @param symbolScheme the scheme to use for serializing the passed references (i.e. defines the separators to use
* between the Entity types, and the characters to escape and how to escape them)
*/
public AbstractStringEntityReferenceResolver(SymbolScheme symbolScheme)
{
this.symbolScheme = symbolScheme;
initialize();
}
@Override
public void initialize()
{
this.referenceSetup = new HashMap<>();
Map<EntityType, Map<EntityType, Character>> separators = getSymbolScheme().getSeparatorSymbols();
for (Map.Entry<EntityType, Map<EntityType, Character>> separatorEntry : separators.entrySet()) {
Map<Character, EntityType> characterMap = new HashMap<>();
for (Map.Entry<EntityType, Character> characterEntry : separatorEntry.getValue().entrySet()) {
characterMap.put(characterEntry.getValue(), characterEntry.getKey());
}
this.referenceSetup.put(separatorEntry.getKey(), characterMap);
}
String escape = Character.toString(getSymbolScheme().getEscapeSymbol());
this.escapeMatching = new String[] { escape + escape, escape };
this.escapeMatchingReplace = new String[] { escape, StringUtils.EMPTY };
}
@Override
public EntityReference resolve(String entityReferenceRepresentation, EntityType type, Object... parameters)
{
Map<Character, EntityType> typeSetup = getTypeSetup(type);
// Check if the type require anything specific
if (typeSetup == null) {
return getEscapedReference(entityReferenceRepresentation, type, parameters);
}
// Handle the case when the passed representation is null. In this case we consider it similar to passing
// an empty string.
StringBuilder representation;
if (entityReferenceRepresentation == null) {
representation = new StringBuilder();
} else {
representation = new StringBuilder(entityReferenceRepresentation);
}
EntityReference reference = null;
EntityType currentType = type;
while (typeSetup != null && !typeSetup.isEmpty()) {
// Search all characters for a non escaped separator. If found, then consider the part after the
// character as the reference name and continue parsing the part before the separator.
EntityType parentType = null;
int i = representation.length();
while (--i >= 0) {
char currentChar = representation.charAt(i);
int nextIndex = i - 1;
char nextChar = 0;
if (nextIndex >= 0) {
nextChar = representation.charAt(nextIndex);
}
if (typeSetup.containsKey(currentChar)) {
int numberOfEscapeChars =
getNumberOfCharsBefore(getSymbolScheme().getEscapeSymbol(), representation, nextIndex);
if (numberOfEscapeChars % 2 == 0) {
parentType = typeSetup.get(currentChar);
break;
} else {
// Unescape the character
representation.delete(nextIndex, i);
--i;
}
} else if (nextChar == getSymbolScheme().getEscapeSymbol()) {
// Unescape the character
representation.delete(nextIndex, i);
--i;
}
}
reference = appendNewReference(reference, getNewReference(i, representation, currentType, parameters));
if (parentType != null) {
currentType = parentType;
} else {
currentType = typeSetup.values().iterator().next();
}
typeSetup = getTypeSetup(currentType);
}
// Handle last entity reference's name
reference = appendNewReference(reference, getEscapedReference(representation, currentType, parameters));
return reference;
}
/**
* The default is extracted from the default {@link SymbolScheme}, but extending classes can override it.
*
* @param type the type for which to get the setup
* @return the reference setup map for the requested type, consisting of <parent separator, parent type> pairs
* @since 7.4.1
* @since 8.0M1
*/
protected Map<Character, EntityType> getTypeSetup(EntityType type)
{
return this.referenceSetup.get(type);
}
private EntityReference getEscapedReference(CharSequence representation, EntityType type, Object... parameters)
{
EntityReference newReference;
if (representation.length() > 0) {
String name =
StringUtils.replaceEach(representation.toString(), this.escapeMatching, this.escapeMatchingReplace);
if (name != null) {
newReference = new EntityReference(name, type);
} else {
newReference = null;
}
} else {
newReference = resolveDefaultReference(type, parameters);
}
return newReference;
}
private EntityReference getNewReference(int i, StringBuilder representation, EntityType type, Object... parameters)
{
EntityReference newReference;
// Found a valid separator (not escaped), separate content on its left from content on its
// right
if (i == representation.length() - 1) {
newReference = resolveDefaultReference(type, parameters);
} else {
String name = representation.substring(i + 1, representation.length());
newReference = new EntityReference(name, type);
}
representation.delete(i < 0 ? 0 : i, representation.length());
return newReference;
}
private EntityReference appendNewReference(EntityReference reference, EntityReference newReference)
{
if (newReference != null) {
if (reference != null) {
return reference.appendParent(newReference);
} else {
return newReference;
}
}
return reference;
}
/**
* Search how many time the provided character is found consecutively started to the provided index and before.
*
* @param c the character to be searched
* @param representation the string being searched
* @param currentPosition the current position where the search is started in backward direction
* @return the number of character in the found group
*/
private int getNumberOfCharsBefore(char c, StringBuilder representation, int currentPosition)
{
int position = currentPosition;
while (position >= 0 && representation.charAt(position) == c) {
--position;
}
return currentPosition - position;
}
protected SymbolScheme getSymbolScheme()
{
return this.symbolScheme;
}
}