/*
* =============================================================================
*
* Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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 org.thymeleaf.engine;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.util.TextUtils;
/**
*
* @author Daniel Fernández
* @since 3.0.0
*
*/
public class AttributeNames {
// We need a different repository for each type of name
private static final AttributeNamesRepository htmlAttributeNamesRepository = new AttributeNamesRepository(TemplateMode.HTML);
private static final AttributeNamesRepository xmlAttributeNamesRepository = new AttributeNamesRepository(TemplateMode.XML);
private static final AttributeNamesRepository textAttributeNamesRepository = new AttributeNamesRepository(TemplateMode.TEXT);
private static TextAttributeName buildTextAttributeName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Attribute name buffer cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Attribute name offset and len must be equal or greater than zero");
}
char c;
int i = attributeNameOffset;
int n = attributeNameLen;
while (n-- != 0) {
c = attributeNameBuffer[i++];
if (c != ':') {
continue;
}
if (c == ':') {
if (i == attributeNameOffset + 1){
// ':' was the first char, no prefix there
return TextAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
return TextAttributeName.forName(
new String(attributeNameBuffer, attributeNameOffset, (i - (attributeNameOffset + 1))),
new String(attributeNameBuffer, i, (attributeNameOffset + attributeNameLen) - i));
}
}
return TextAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
private static XMLAttributeName buildXMLAttributeName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Attribute name buffer cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Attribute name offset and len must be equal or greater than zero");
}
char c;
int i = attributeNameOffset;
int n = attributeNameLen;
while (n-- != 0) {
c = attributeNameBuffer[i++];
if (c != ':') {
continue;
}
if (c == ':') {
if (i == attributeNameOffset + 1){
// ':' was the first char, no prefix there
return XMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
return XMLAttributeName.forName(
new String(attributeNameBuffer, attributeNameOffset, (i - (attributeNameOffset + 1))),
new String(attributeNameBuffer, i, (attributeNameOffset + attributeNameLen) - i));
}
}
return XMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
private static HTMLAttributeName buildHTMLAttributeName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Attribute name buffer cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Attribute name offset and len must be equal or greater than zero");
}
char c;
int i = attributeNameOffset;
int n = attributeNameLen;
boolean inData = false;
while (n-- != 0) {
c = attributeNameBuffer[i++];
if (c != ':' && c != '-') {
continue;
}
if (!inData && c == ':') {
if (i == attributeNameOffset + 1){
// ':' was the first char, no prefix there
return HTMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
if (TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "xml:", 0, 4, attributeNameBuffer, attributeNameOffset, (i - attributeNameOffset)) ||
TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "xmlns:", 0, 6, attributeNameBuffer, attributeNameOffset, (i - attributeNameOffset))) {
// 'xml' and 'xmlns' are not a valid dialect prefix in HTML mode
return HTMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
return HTMLAttributeName.forName(
new String(attributeNameBuffer, attributeNameOffset, (i - (attributeNameOffset + 1))),
new String(attributeNameBuffer, i, (attributeNameOffset + attributeNameLen) - i));
}
if (!inData && c == '-') {
if (i == attributeNameOffset + 5 && TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "data", 0, 4, attributeNameBuffer, attributeNameOffset, (i - (attributeNameOffset + 1)))) {
inData = true;
continue;
} else {
// this is just a normal, non-thymeleaf 'data-*' attribute
return HTMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
}
if (inData && c == '-') {
if (i == attributeNameOffset + 6) {
// '-' was the first char after 'data-', no prefix there
return HTMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
return HTMLAttributeName.forName(
new String(attributeNameBuffer, attributeNameOffset + 5, (i - (attributeNameOffset + 6))),
new String(attributeNameBuffer, i, (attributeNameOffset + attributeNameLen) - i));
}
}
return HTMLAttributeName.forName(null, new String(attributeNameBuffer, attributeNameOffset, attributeNameLen));
}
private static TextAttributeName buildTextAttributeName(final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
char c;
int i = 0;
int n = attributeName.length();
while (n-- != 0) {
c = attributeName.charAt(i++);
if (c != ':') {
continue;
}
if (c == ':') {
if (i == 1){
// ':' was the first char, no prefix there
return TextAttributeName.forName(null, attributeName);
}
return TextAttributeName.forName(
attributeName.substring(0, i - 1),
attributeName.substring(i, attributeName.length()));
}
}
return TextAttributeName.forName(null, attributeName);
}
private static XMLAttributeName buildXMLAttributeName(final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
char c;
int i = 0;
int n = attributeName.length();
while (n-- != 0) {
c = attributeName.charAt(i++);
if (c != ':') {
continue;
}
if (c == ':') {
if (i == 1){
// ':' was the first char, no prefix there
return XMLAttributeName.forName(null, attributeName);
}
return XMLAttributeName.forName(
attributeName.substring(0, i - 1),
attributeName.substring(i, attributeName.length()));
}
}
return XMLAttributeName.forName(null, attributeName);
}
private static HTMLAttributeName buildHTMLAttributeName(final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
char c;
int i = 0;
int n = attributeName.length();
boolean inData = false;
while (n-- != 0) {
c = attributeName.charAt(i++);
if (c != ':' && c != '-') {
continue;
}
if (!inData && c == ':') {
if (i == 1){
// ':' was the first char, no prefix there
return HTMLAttributeName.forName(null, attributeName);
}
if (TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "xml:", 0, 4, attributeName, 0, i) ||
TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "xmlns:", 0, 6, attributeName, 0, i)) {
// 'xml' is not a valid dialect prefix in HTML mode
return HTMLAttributeName.forName(null, attributeName);
}
return HTMLAttributeName.forName(
attributeName.substring(0, i - 1),
attributeName.substring(i,attributeName.length()));
}
if (!inData && c == '-') {
if (i == 5 && TextUtils.equals(TemplateMode.HTML.isCaseSensitive(), "data", 0, 4, attributeName, 0, 4)) {
inData = true;
continue;
} else {
// this is just a normal, non-thymeleaf 'data-*' attribute
return HTMLAttributeName.forName(null, attributeName);
}
}
if (inData && c == '-') {
if (i == 6) {
// '-' was the first char after 'data-', no prefix there
return HTMLAttributeName.forName(null, attributeName);
}
return HTMLAttributeName.forName(
attributeName.substring(5, i - 1),
attributeName.substring(i, attributeName.length()));
}
}
return HTMLAttributeName.forName(null, attributeName);
}
private static TextAttributeName buildTextAttributeName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
if (prefix == null || prefix.trim().length() == 0) {
return buildTextAttributeName(attributeName);
}
return TextAttributeName.forName(prefix, attributeName);
}
private static XMLAttributeName buildXMLAttributeName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
if (prefix == null || prefix.trim().length() == 0) {
return buildXMLAttributeName(attributeName);
}
return XMLAttributeName.forName(prefix, attributeName);
}
private static HTMLAttributeName buildHTMLAttributeName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.length() == 0) {
throw new IllegalArgumentException("Attribute name cannot be null or empty");
}
if (prefix == null || prefix.trim().length() == 0) {
return buildHTMLAttributeName(attributeName);
}
return HTMLAttributeName.forName(prefix, attributeName);
}
public static AttributeName forName(
final TemplateMode templateMode, final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (templateMode == null) {
throw new IllegalArgumentException("Template Mode cannot be null");
}
if (templateMode == TemplateMode.HTML) {
return forHTMLName(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
if (templateMode == TemplateMode.XML) {
return forXMLName(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
if (templateMode.isText()) {
return forTextName(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
throw new IllegalArgumentException("Unknown template mode '" + templateMode + "'");
}
public static AttributeName forName(final TemplateMode templateMode, final String attributeName) {
if (templateMode == null) {
throw new IllegalArgumentException("Template Mode cannot be null");
}
if (templateMode == TemplateMode.HTML) {
return forHTMLName(attributeName);
}
if (templateMode == TemplateMode.XML) {
return forXMLName(attributeName);
}
if (templateMode.isText()) {
return forTextName(attributeName);
}
throw new IllegalArgumentException("Unknown template mode '" + templateMode + "'");
}
public static AttributeName forName(final TemplateMode templateMode, final String prefix, final String attributeName) {
if (templateMode == null) {
throw new IllegalArgumentException("Template Mode cannot be null");
}
if (templateMode == TemplateMode.HTML) {
return forHTMLName(prefix, attributeName);
}
if (templateMode == TemplateMode.XML) {
return forXMLName(prefix, attributeName);
}
if (templateMode.isText()) {
return forTextName(prefix, attributeName);
}
throw new IllegalArgumentException("Unknown template mode '" + templateMode + "'");
}
public static TextAttributeName forTextName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Both name offset and length must be equal to or greater than zero");
}
return (TextAttributeName) textAttributeNamesRepository.getAttribute(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
public static XMLAttributeName forXMLName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Both name offset and length must be equal to or greater than zero");
}
return (XMLAttributeName) xmlAttributeNamesRepository.getAttribute(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
public static HTMLAttributeName forHTMLName(final char[] attributeNameBuffer, final int attributeNameOffset, final int attributeNameLen) {
if (attributeNameBuffer == null || attributeNameLen == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
if (attributeNameOffset < 0 || attributeNameLen < 0) {
throw new IllegalArgumentException("Both name offset and length must be equal to or greater than zero");
}
return (HTMLAttributeName) htmlAttributeNamesRepository.getAttribute(attributeNameBuffer, attributeNameOffset, attributeNameLen);
}
public static TextAttributeName forTextName(final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (TextAttributeName) textAttributeNamesRepository.getAttribute(attributeName);
}
public static XMLAttributeName forXMLName(final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (XMLAttributeName) xmlAttributeNamesRepository.getAttribute(attributeName);
}
public static HTMLAttributeName forHTMLName(final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (HTMLAttributeName) htmlAttributeNamesRepository.getAttribute(attributeName);
}
public static TextAttributeName forTextName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (TextAttributeName) textAttributeNamesRepository.getAttribute(prefix, attributeName);
}
public static XMLAttributeName forXMLName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (XMLAttributeName) xmlAttributeNamesRepository.getAttribute(prefix, attributeName);
}
public static HTMLAttributeName forHTMLName(final String prefix, final String attributeName) {
if (attributeName == null || attributeName.trim().length() == 0) {
throw new IllegalArgumentException("Name cannot be null or empty");
}
return (HTMLAttributeName) htmlAttributeNamesRepository.getAttribute(prefix, attributeName);
}
private AttributeNames() {
super();
}
/*
* This repository class is thread-safe, as it will contain new instances of AttributeName created during
* processing (created when asking the repository for them when they do not exist yet). As any thread can
* create a new attribute, this has to be lock-protected.
*/
static final class AttributeNamesRepository {
private final TemplateMode templateMode;
private final List<String> repositoryNames; // read-write, sync will be needed
private final List<AttributeName> repository; // read-write, sync will be needed
private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
private final Lock readLock = this.lock.readLock();
private final Lock writeLock = this.lock.writeLock();
AttributeNamesRepository(final TemplateMode templateMode) {
super();
this.templateMode = templateMode;
this.repositoryNames = new ArrayList<String>(500);
this.repository = new ArrayList<AttributeName>(500);
}
AttributeName getAttribute(final char[] text, final int offset, final int len) {
int index;
this.readLock.lock();
try {
/*
* First look for the element in the namespaced repository
*/
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, text, offset, len);
if (index >= 0) {
return this.repository.get(index);
}
} finally {
this.readLock.unlock();
}
/*
* NOT FOUND. We need to obtain a write lock and store the text
*/
this.writeLock.lock();
try {
return storeAttribute(text, offset, len);
} finally {
this.writeLock.unlock();
}
}
AttributeName getAttribute(final String completeAttributeName) {
int index;
this.readLock.lock();
try {
/*
* First look for the element in the namespaced repository
*/
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, completeAttributeName);
if (index >= 0) {
return this.repository.get(index);
}
} finally {
this.readLock.unlock();
}
/*
* NOT FOUND. We need to obtain a write lock and store the text
*/
this.writeLock.lock();
try {
return storeAttribute(completeAttributeName);
} finally {
this.writeLock.unlock();
}
}
AttributeName getAttribute(final String prefix, final String attributeName) {
int index;
this.readLock.lock();
try {
/*
* First look for the element in the namespaced repository
*/
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, prefix, attributeName);
if (index >= 0) {
return this.repository.get(index);
}
} finally {
this.readLock.unlock();
}
/*
* NOT FOUND. We need to obtain a write lock and store the text
*/
this.writeLock.lock();
try {
return storeAttribute(prefix, attributeName);
} finally {
this.writeLock.unlock();
}
}
private AttributeName storeAttribute(final char[] text, final int offset, final int len) {
int index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, text, offset, len);
if (index >= 0) {
// It was already added while we were waiting for the lock!
return this.repository.get(index);
}
final AttributeName name;
if (this.templateMode == TemplateMode.HTML) {
name = buildHTMLAttributeName(text, offset, len);
} else if (this.templateMode == TemplateMode.XML) {
name = buildXMLAttributeName(text, offset, len);
} else { // this.templateMode.isText()
name = buildTextAttributeName(text, offset, len);
}
final String[] completeAttributeNames = name.completeAttributeNames;
for (final String completeAttributeName : completeAttributeNames) {
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, completeAttributeName);
// binary Search returned (-(insertion point) - 1)
this.repositoryNames.add(((index + 1) * -1), completeAttributeName);
this.repository.add(((index + 1) * -1), name);
}
return name;
}
private AttributeName storeAttribute(final String attributeName) {
int index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, attributeName);
if (index >= 0) {
// It was already added while we were waiting for the lock!
return this.repository.get(index);
}
final AttributeName name;
if (this.templateMode == TemplateMode.HTML) {
name = buildHTMLAttributeName(attributeName);
} else if (this.templateMode == TemplateMode.XML) {
name = buildXMLAttributeName(attributeName);
} else { // this.templateMode.isText()
name = buildTextAttributeName(attributeName);
}
final String[] completeAttributeNames = name.completeAttributeNames;
for (final String completeAttributeName : completeAttributeNames) {
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, completeAttributeName);
// binary Search returned (-(insertion point) - 1)
this.repositoryNames.add(((index + 1) * -1), completeAttributeName);
this.repository.add(((index + 1) * -1), name);
}
return name;
}
private AttributeName storeAttribute(final String prefix, final String attributeName) {
int index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, prefix, attributeName);
if (index >= 0) {
// It was already added while we were waiting for the lock!
return this.repository.get(index);
}
final AttributeName name;
if (this.templateMode == TemplateMode.HTML) {
name = buildHTMLAttributeName(prefix, attributeName);
} else if (this.templateMode == TemplateMode.XML) {
name = buildXMLAttributeName(prefix, attributeName);
} else { // this.templateMode.isText()
name = buildTextAttributeName(prefix, attributeName);
}
final String[] completeAttributeNames = name.completeAttributeNames;
for (final String completeAttributeName : completeAttributeNames) {
index = binarySearch(this.templateMode.isCaseSensitive(), this.repositoryNames, completeAttributeName);
// binary Search returned (-(insertion point) - 1)
this.repositoryNames.add(((index + 1) * -1), completeAttributeName);
this.repository.add(((index + 1) * -1), name);
}
return name;
}
private static int binarySearch(
final boolean caseSensitive, final List<String> values, final char[] text, final int offset, final int len) {
int low = 0;
int high = values.size() - 1;
int mid, cmp;
String midVal;
while (low <= high) {
mid = (low + high) >>> 1;
midVal = values.get(mid);
cmp = TextUtils.compareTo(caseSensitive, midVal, 0, midVal.length(), text, offset, len);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
// Found!!
return mid;
}
}
return -(low + 1); // Not Found!! We return (-(insertion point) - 1), to guarantee all non-founds are < 0
}
private static int binarySearch(final boolean caseSensitive, final List<String> values, final String text) {
int low = 0;
int high = values.size() - 1;
int mid, cmp;
String midVal;
while (low <= high) {
mid = (low + high) >>> 1;
midVal = values.get(mid);
cmp = TextUtils.compareTo(caseSensitive, midVal, text);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
// Found!!
return mid;
}
}
return -(low + 1); // Not Found!! We return (-(insertion point) - 1), to guarantee all non-founds are < 0
}
private static int binarySearch(final boolean caseSensitive,
final List<String> values, final String prefix, final String attributeName) {
// This method will be specialized in finding prefixed attribute names (in the prefix:name form)
if (prefix == null || prefix.trim().length() == 0) {
return binarySearch(caseSensitive, values, attributeName);
}
final int prefixLen = prefix.length();
final int attributeNameLen = attributeName.length();
int low = 0;
int high = values.size() - 1;
int mid, cmp;
String midVal;
int midValLen;
while (low <= high) {
mid = (low + high) >>> 1;
midVal = values.get(mid);
midValLen = midVal.length();
if (TextUtils.startsWith(caseSensitive, midVal, prefix)) {
// Prefix matched, but it could be a mere coincidence if the text being evaluated doesn't have
// a ':' after the prefix letters, so we will make sure by comparing the next char manually
if (midValLen <= prefixLen) {
// midVal is exactly as prefix, therefore it goes first
low = mid + 1;
} else {
// Compare the next char
cmp = midVal.charAt(prefixLen) - ':';
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
// Prefix matches and we made sure midVal has a ':', so let's try the attributeName
cmp = TextUtils.compareTo(caseSensitive, midVal, prefixLen + 1, (midValLen - (prefixLen + 1)), attributeName, 0, attributeNameLen);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
// Found!!
return mid;
}
}
}
} else {
// midVal does not start with prefix, so comparing midVal and prefix should be enough
cmp = TextUtils.compareTo(caseSensitive, midVal, prefix);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
// This is impossible - if they were the same, we'd have detected it already!
throw new IllegalStateException("Bad comparison of midVal \"" + midVal + "\" and prefix \"" + prefix + "\"");
}
}
}
return -(low + 1); // Not Found!! We return (-(insertion point) - 1), to guarantee all non-founds are < 0
}
}
}