/* * ============================================================================= * * 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.io.IOException; import java.io.Writer; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import org.thymeleaf.exceptions.TemplateProcessingException; import org.thymeleaf.model.AttributeValueQuotes; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.util.FastStringWriter; import org.thymeleaf.util.Validate; /** * * @author Daniel Fernández * @since 3.0.0 * */ final class Attributes { static final String DEFAULT_WHITE_SPACE = " "; static final String[] DEFAULT_WHITE_SPACE_ARRAY = new String[] { DEFAULT_WHITE_SPACE }; static final Attributes EMPTY_ATTRIBUTES = new Attributes(null, null); static final Attribute[] EMPTY_ATTRIBUTE_ARRAY = new Attribute[0]; final Attribute[] attributes; // might be null if there are no attributes final String[] innerWhiteSpaces; // might be null if there are no attributes and no whitespaces private volatile int associatedProcessorCount = -1; Attributes(final Attribute[] attributes, final String[] innerWhiteSpaces) { super(); this.attributes = attributes; this.innerWhiteSpaces = innerWhiteSpaces; } int getAssociatedProcessorCount() { int c = this.associatedProcessorCount; if (c < 0) { this.associatedProcessorCount = c = computeAssociatedProcessorCount(); } return c; } private int computeAssociatedProcessorCount() { if (this.attributes == null || this.attributes.length == 0) { return 0; } int count = 0; int n = this.attributes.length; while (n-- != 0) { if (this.attributes[n].definition.hasAssociatedProcessors) { count += this.attributes[n].definition.associatedProcessors.length; } } return count; } private int searchAttribute(final TemplateMode templateMode, final String completeName) { if (this.attributes == null || this.attributes.length == 0) { return -1; } // We will first try exact match on the names with which the attributes appear on template, as an optimization // on the base case (use the AttributeDefinition). int n = this.attributes.length; while (n-- != 0) { if (this.attributes[n].completeName.equals(completeName)) { return n; } } // Not found that way - before discarding, let's search using AttributeDefinitions return searchAttribute(AttributeNames.forName(templateMode, completeName)); } private int searchAttribute(final TemplateMode templateMode, final String prefix, final String name) { if (this.attributes == null || this.attributes.length == 0) { return -1; } if (prefix == null || prefix.length() == 0) { // Optimization: searchAttribute(name) might be faster if we are able to avoid using AttributeDefinition return searchAttribute(templateMode, name); } return searchAttribute(AttributeNames.forName(templateMode, prefix, name)); } private int searchAttribute(final AttributeName attributeName) { if (this.attributes == null || this.attributes.length == 0) { return -1; } int n = this.attributes.length; while (n-- != 0) { // AttributeName objects are registered in a repository and are singletons, so == is fine if (this.attributes[n].definition.attributeName == attributeName) { return n; } } return -1; } boolean hasAttribute(final TemplateMode templateMode, final String completeName) { return searchAttribute(templateMode, completeName) >= 0; } boolean hasAttribute(final TemplateMode templateMode, final String prefix, final String name) { return searchAttribute(templateMode, prefix, name) >= 0; } boolean hasAttribute(final AttributeName attributeName) { return searchAttribute(attributeName) >= 0; } Attribute getAttribute(final TemplateMode templateMode, final String completeName) { final int pos = searchAttribute(templateMode, completeName); if (pos < 0) { return null; } return this.attributes[pos]; } Attribute getAttribute(final TemplateMode templateMode, final String prefix, final String name) { final int pos = searchAttribute(templateMode, prefix, name); if (pos < 0) { return null; } return this.attributes[pos]; } Attribute getAttribute(final AttributeName attributeName) { final int pos = searchAttribute(attributeName); if (pos < 0) { return null; } return this.attributes[pos]; } Attribute[] getAllAttributes() { if (this.attributes == null || this.attributes.length == 0) { return EMPTY_ATTRIBUTE_ARRAY; } // We will be performing defensive cloning here. Still sleeker than returning an immutable Set or similar return this.attributes.clone(); } Map<String,String> getAttributeMap() { if (this.attributes == null || this.attributes.length == 0) { return Collections.emptyMap(); } final Map<String,String> attributeMap = new LinkedHashMap<String, String>(this.attributes.length + 5); for (int i = 0; i < this.attributes.length; i++) { attributeMap.put(this.attributes[i].completeName, this.attributes[i].value); } return attributeMap; } Attributes setAttribute( final AttributeDefinitions attributeDefinitions, final TemplateMode templateMode, final AttributeDefinition attributeDefinition, final String completeName, final String value, final AttributeValueQuotes valueQuotes) { // attributeDefinition might be null if it wasn't available at the moment of calling this method // (including it is basically an optimization for classes that can work against engine implementations) Validate.isTrue(value != null || templateMode != TemplateMode.XML, "Cannot set null-value attributes in XML template mode"); Validate.isTrue(valueQuotes != AttributeValueQuotes.NONE || templateMode != TemplateMode.XML, "Cannot set unquoted attributes in XML template mode"); final int existingIdx = (attributeDefinition != null? searchAttribute(attributeDefinition.attributeName) : searchAttribute(templateMode, completeName)); if (existingIdx >= 0) { // Attribute already exists! Must simply change its properties (might include a name case change!) final Attribute[] newAttributes = this.attributes.clone(); newAttributes[existingIdx] = newAttributes[existingIdx].modify(null, completeName, value, valueQuotes); return new Attributes(newAttributes, this.innerWhiteSpaces); } final AttributeDefinition newAttributeDefinition = (attributeDefinition != null ? attributeDefinition : attributeDefinitions.forName(templateMode, completeName)); final Attribute newAttribute = new Attribute(newAttributeDefinition, completeName, null, value, valueQuotes, null, -1, -1); final Attribute[] newAttributes; if (this.attributes != null) { newAttributes = new Attribute[this.attributes.length + 1]; System.arraycopy(this.attributes, 0, newAttributes, 0, this.attributes.length); newAttributes[this.attributes.length] = newAttribute; } else { newAttributes = new Attribute[] { newAttribute }; } final String[] newInnerWhiteSpaces; if (this.innerWhiteSpaces != null) { newInnerWhiteSpaces = new String[this.innerWhiteSpaces.length + 1]; System.arraycopy(this.innerWhiteSpaces, 0, newInnerWhiteSpaces, 0, this.innerWhiteSpaces.length); if (this.innerWhiteSpaces.length == (this.attributes != null? this.attributes.length : 0)) { // As many inner white spaces as attributes: no white space is being left after the last attribute newInnerWhiteSpaces[this.innerWhiteSpaces.length] = DEFAULT_WHITE_SPACE; } else { // There is a white space after the last attribute, so we are going to respect it newInnerWhiteSpaces[this.innerWhiteSpaces.length] = newInnerWhiteSpaces[this.innerWhiteSpaces.length - 1]; newInnerWhiteSpaces[this.innerWhiteSpaces.length - 1] = DEFAULT_WHITE_SPACE; } } else { newInnerWhiteSpaces = DEFAULT_WHITE_SPACE_ARRAY; } return new Attributes(newAttributes, newInnerWhiteSpaces); } Attributes replaceAttribute( final AttributeDefinitions attributeDefinitions, final TemplateMode templateMode, final AttributeName oldName, final AttributeDefinition newAttributeDefinition, final String newCompleteName, final String value, final AttributeValueQuotes valueQuotes) { // attributeDefinition might be null if it wasn't available at the moment of calling this method // (including it is basically an optimization for classes that can work against engine implementations) Validate.isTrue(value != null || templateMode != TemplateMode.XML, "Cannot set null-value attributes in XML template mode"); Validate.isTrue(valueQuotes != AttributeValueQuotes.NONE || templateMode != TemplateMode.XML, "Cannot set unquoted attributes in XML template mode"); if (this.attributes == null) { return setAttribute(attributeDefinitions, templateMode, newAttributeDefinition, newCompleteName, value, valueQuotes); } // First check existence of the old one -- if it does not exist, this is exactly the same as setAttribute final int oldIdx = searchAttribute(oldName); if (oldIdx < 0) { return setAttribute(attributeDefinitions, templateMode, newAttributeDefinition, newCompleteName, value, valueQuotes); } // Now check existence of the new one -- if it does exist, we will try to reuse its fields (even if the old one exists too) int existingIdx = (newAttributeDefinition != null? searchAttribute(newAttributeDefinition.attributeName) : searchAttribute(templateMode, newCompleteName)); if (existingIdx >= 0) { if (oldIdx == existingIdx) { // Old and new are the same -- this is a setAttribute return setAttribute(attributeDefinitions, templateMode, newAttributeDefinition, newCompleteName, value, valueQuotes); } final Attribute[] newAttributes = new Attribute[this.attributes.length - 1]; // Do remove the old attribute System.arraycopy(this.attributes, 0, newAttributes, 0, oldIdx); System.arraycopy(this.attributes, oldIdx + 1, newAttributes, oldIdx, newAttributes.length - oldIdx); // Let's compute the index of the white space to be removed. In general, we will remove the white space AFTER int iwIdx = oldIdx + 1; if (oldIdx + 1 == this.attributes.length) { // We've just removed the last attribute --- in this case we prefer to remove the white space BEFORE the // removed attribute, even if a final white space exists after this attribute iwIdx = oldIdx; } final String[] newInnerWhiteSpaces = new String[this.innerWhiteSpaces.length - 1]; System.arraycopy(this.innerWhiteSpaces, 0, newInnerWhiteSpaces, 0, iwIdx); System.arraycopy(this.innerWhiteSpaces, iwIdx + 1, newInnerWhiteSpaces, iwIdx, newInnerWhiteSpaces.length - iwIdx); // After removing the old attribute, the position of the new one might have changed if (existingIdx > oldIdx) { existingIdx--; } // Now modify the existing new attribute directly in the new array newAttributes[existingIdx] = newAttributes[existingIdx].modify(null, newCompleteName, value, valueQuotes); return new Attributes(newAttributes, newInnerWhiteSpaces); } // By now we know the old one exists, but the new one doesn't, so let's simply replace the old one with the new one final AttributeDefinition computedNewAttributeDefinition = (newAttributeDefinition != null ? newAttributeDefinition : attributeDefinitions.forName(templateMode, newCompleteName)); // We will 'modify' the old one, even if we are going to change the name, so that transition from the old name // to the new one is smoother by keeping (if allowed) operator, value quotes, etc. final Attribute[] newAttributes = this.attributes.clone(); newAttributes[oldIdx] = newAttributes[oldIdx].modify(computedNewAttributeDefinition, newCompleteName, value, valueQuotes); return new Attributes(newAttributes, this.innerWhiteSpaces); } Attributes removeAttribute(final TemplateMode templateMode, final String prefix, final String name) { if (this.attributes == null) { // We have no attribute array, nothing to remove return this; } final int attrIdx = searchAttribute(templateMode, prefix, name); if (attrIdx < 0) { // Attribute does not exist. Just exit return this; } return removeAttribute(attrIdx); } Attributes removeAttribute(final TemplateMode templateMode, final String completeName) { if (this.attributes == null) { // We have no attribute array, nothing to remove return this; } final int attrIdx = searchAttribute(templateMode, completeName); if (attrIdx < 0) { // Attribute does not exist. Just exit return this; } return removeAttribute(attrIdx); } Attributes removeAttribute(final AttributeName attributeName) { if (this.attributes == null) { // We have no attribute array, nothing to remove return this; } final int attrIdx = searchAttribute(attributeName); if (attrIdx < 0) { // Attribute does not exist. Just exit return this; } return removeAttribute(attrIdx); } private Attributes removeAttribute(final int attrIdx) { if (this.attributes.length == 1 && this.innerWhiteSpaces.length == 1) { // We are removing the last attribute and there is no extra white space: use the EMPTY constant return EMPTY_ATTRIBUTES; } final Attribute[] newAttributes; if (this.attributes.length == 1) { newAttributes = null; } else { newAttributes = new Attribute[this.attributes.length - 1]; System.arraycopy(this.attributes, 0, newAttributes, 0, attrIdx); System.arraycopy(this.attributes, attrIdx + 1, newAttributes, attrIdx, newAttributes.length - attrIdx); } // Let's compute the index of the white space to be removed. In general, we will remove the white space AFTER int iwIdx = attrIdx + 1; if (attrIdx + 1 == this.attributes.length) { // We've just removed the last attribute --- in this case we prefer to remove the white space BEFORE the // removed attribute, even if a final white space exists after this attribute iwIdx = attrIdx; } final String[] newInnerWhiteSpaces = new String[this.innerWhiteSpaces.length - 1]; System.arraycopy(this.innerWhiteSpaces, 0, newInnerWhiteSpaces, 0, iwIdx); System.arraycopy(this.innerWhiteSpaces, iwIdx + 1, newInnerWhiteSpaces, iwIdx, newInnerWhiteSpaces.length - iwIdx); return new Attributes(newAttributes, newInnerWhiteSpaces); } void write(final Writer writer) throws IOException { if (this.attributes == null) { if (this.innerWhiteSpaces != null) { // In this case, there will be only one white space writer.write(this.innerWhiteSpaces[0]); } return; } int i = 0; for (; i < this.attributes.length; i++) { writer.write(this.innerWhiteSpaces[i]); this.attributes[i].write(writer); } // There might be a final whitespace after the last attribute if (i < this.innerWhiteSpaces.length) { writer.write(this.innerWhiteSpaces[i]); } } @Override public String toString() { final Writer stringWriter = new FastStringWriter(); try { write(stringWriter); } catch (final IOException e) { throw new TemplateProcessingException("Exception processing String form of ElementAttributes", e); } return stringWriter.toString(); } }