/*
* =============================================================================
*
* 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.Arrays;
import org.thymeleaf.exceptions.TemplateProcessingException;
import org.thymeleaf.processor.element.IElementProcessor;
import org.thymeleaf.util.ProcessorComparators;
/**
*
* @author Daniel Fernández
* @since 3.0.0
*
*/
final class ElementProcessorIterator {
/*
* This class will take care of iterating the processors in the most optimal way possible. It allows the attributes
* in the tag to be modified durint iteration, taking new processors into account as soon as they appear, even if
* they have higher precedence than the last executed processor for the same tag.
*/
private int last = -1;
// These are the structures used to keep track of the iterated processors, as well as whether they have
// been visited or not.
private IElementProcessor[] processors = null;
private boolean[] visited = null;
private int size = 0;
// These structures are used when we need to recompute already-existing structures, in order to reduce
// the total amount of processor arrays created during normal operation (attributes might change a lot).
private IElementProcessor[] auxProcessors = null;
private boolean[] auxVisited = null;
private int auxSize = 0;
// The version, used to keep track of the tag's attributes and knowing when we have to recompute
private AbstractProcessableElementTag currentTag = null;
// This flag will determine if we should return the last processor that we have already returned, or if we just did
private boolean lastToBeRepeated = false;
private boolean lastWasRepeated = false;
ElementProcessorIterator() {
super();
}
void reset() {
this.size = 0;
this.last = -1;
this.currentTag = null;
this.lastToBeRepeated = false;
this.lastWasRepeated = false;
}
IElementProcessor next(final AbstractProcessableElementTag tag) {
// It should never happen that after calling 'setLastToBeRepeated' we change tag or modify it before
// calling 'next' again, so we are fine checking this flag before checking for recomputes
if (this.lastToBeRepeated) {
final IElementProcessor repeatedLast = computeRepeatedLast(tag);
this.lastToBeRepeated = false;
this.lastWasRepeated = true;
return repeatedLast;
}
this.lastWasRepeated = false;
if (this.currentTag != tag) { // tags are immutable, so we will use them as a marker of being updated
recompute(tag);
this.currentTag = tag;
this.last = -1;
}
if (this.processors == null) {
return null;
}
// We use 'last' as a starting index in order save some iterations (except after recomputes)
int i = this.last + 1;
int n = this.size - i;
while (n-- != 0) {
if (!this.visited[i]) {
this.visited[i] = true;
this.last = i;
return this.processors[i];
}
i++;
}
this.last = this.size;
return null;
}
private IElementProcessor computeRepeatedLast(final AbstractProcessableElementTag tag) {
if (this.currentTag != tag) {
throw new TemplateProcessingException("Cannot return last processor to be repeated: changes were made and processor recompute is needed!");
}
if (this.processors == null) {
throw new TemplateProcessingException("Cannot return last processor to be repeated: no processors in tag!");
}
return this.processors[this.last];
}
boolean lastWasRepeated() {
return this.lastWasRepeated;
}
void setLastToBeRepeated(final AbstractProcessableElementTag tag) {
if (this.currentTag != tag) {
throw new TemplateProcessingException("Cannot set last processor to be repeated: processor recompute is needed!");
}
if (this.processors == null) {
throw new TemplateProcessingException("Cannot set last processor to be repeated: no processors in tag!");
}
this.lastToBeRepeated = true;
}
private void recompute(final AbstractProcessableElementTag tag) {
// Before recomputing the iterator itself, we have to make sure that the associated processors are up-to-date
final IElementProcessor[] associatedProcessors = tag.getAssociatedProcessors();
if (associatedProcessors.length == 0) {
// After recompute, it seems we have no processors to be applied (we might have had before)
if (this.processors != null) {
// We don't mind what we had in the arrays - setting size to 0 will invalidate them
this.size = 0;
} // else there's nothing to do -- we had nothing precomputed, and will still have the same nothing
return;
}
if (this.processors == null) {
// We had nothing precomputed, but there are associated processors now!
this.size = associatedProcessors.length;
this.processors = new IElementProcessor[Math.max(this.size, 4)]; // minimum size = 4
this.visited = new boolean[Math.max(this.size, 4)]; // minimum size = 4
System.arraycopy(associatedProcessors, 0, this.processors, 0, this.size);
Arrays.fill(this.visited, false);
return;
}
// Processors have changed since the last time we used the iterator (attributes changed),
// so we need to use the 'aux' structures in order to recompute processors and then swap.
this.auxSize = associatedProcessors.length;
if (this.auxProcessors == null || this.auxSize > this.auxProcessors.length) {
// We need new aux arrays (either don't exist, or they are too small)
this.auxProcessors = new IElementProcessor[Math.max(this.auxSize, 4)];
this.auxVisited = new boolean[Math.max(this.auxSize, 4)];
}
System.arraycopy(associatedProcessors, 0, this.auxProcessors, 0, this.auxSize);
// No pre-initialization for the visited array -- we will do it position by position
// Now we should check the matches between the new and the old iterator processors - we will build
// on the fact that processors are always ordered by precedence
int i = 0; // index for the NEW processors
int j = 0; // index for the OLD processors
while (i < this.auxSize) {
if (i >= this.size || j >= this.size) {
// We know everything in the new array from here on has to be new. Might also be that we
// just did a resetGathering (this.size == 0), and we are going to consider every new processor
// as "not visited"
Arrays.fill(this.auxVisited, i, this.auxSize, false);
break;
}
if (this.auxProcessors[i] == this.processors[j]) {
this.auxVisited[i] = this.visited[j];
i++;
j++;
continue;
}
// Doesn't match. Either we have a new processor, or an previous one was removed
final int comp = ProcessorComparators.PROCESSOR_COMPARATOR.compare(this.auxProcessors[i], this.processors[j]);
if (comp == 0) {
// This should never happen. The comparator should make sure the only case in which comp == 0 is when
// processors are the same (i.e. the object)
throw new IllegalStateException(
"Two different registered processors have returned zero as a result of their " +
"comparison, which is forbidden. Offending processors are " +
this.auxProcessors[i].getClass().getName() + " and " +
this.processors[j].getClass().getName());
} else if (comp < 0) {
// The new one has higher precedence (lower value), so it's new
this.auxVisited[i] = false; // We need to execute this for sure! (it's new)
i++;
// continue
} else { // comp > 0
// The old one has higher precedence (lower value), so it's been removed -- just skip
j++;
// continue
}
}
// Finally, just swap the arrays
final IElementProcessor[] swapProcessors = this.auxProcessors;
final boolean[] swapVisited = this.auxVisited;
this.auxProcessors = this.processors;
this.auxVisited = this.visited;
this.processors = swapProcessors;
this.visited = swapVisited;
this.size = this.auxSize;
}
void resetAsCloneOf(final ElementProcessorIterator original) {
this.size = original.size;
this.last = original.last;
this.currentTag = original.currentTag;
this.lastToBeRepeated = original.lastToBeRepeated;
this.lastWasRepeated = original.lastWasRepeated;
if (this.size > 0 && original.processors != null) { // original.visited will also be != null
if (this.processors == null || this.processors.length < this.size) {
this.processors = new IElementProcessor[this.size];
this.visited = new boolean[this.size];
}
System.arraycopy(original.processors, 0, this.processors, 0, this.size);
System.arraycopy(original.visited, 0, this.visited, 0, this.size);
}
}
}