blob: 793f0d31bfaf3f64a0db61353a5c3b5196d5d028 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.freemarker.core;
import org.apache.freemarker.core.arithmetic.ArithmeticEngine;
import org.apache.freemarker.core.model.*;
import org.apache.freemarker.core.model.impl.SequenceTemplateModelIterator;
import org.apache.freemarker.core.model.impl.SimpleNumber;
import org.apache.freemarker.core.model.impl.SimpleString;
import org.apache.freemarker.core.model.impl.TemplateModelListSequence;
import org.apache.freemarker.core.util.BugException;
import org.apache.freemarker.core.util._StringUtils;
import java.io.Serializable;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import static org.apache.freemarker.core.util.CallableUtils.*;
/**
* A holder for builtins that operate on sequence (or some even on iterable) left-hand value.
*/
class BuiltInsForSequences {
// TODO [FM3] Should work on TemplateIterableModel as well
static class chunkBI extends BuiltInForSequence {
private class BIMethod extends BuiltInCallableImpl implements TemplateFunctionModel {
private final TemplateSequenceModel tsm;
private BIMethod(TemplateSequenceModel tsm) {
this.tsm = tsm;
}
@Override
public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env)
throws TemplateException {
int chunkSize = getNumberArgument(args, 0, this).intValue();
if (chunkSize < 1) {
newArgumentValueException(0, "The value must be at least 1", this);
}
return new ChunkedSequence(tsm, chunkSize, args[1]);
}
@Override
public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
return ArgumentArrayLayout.TWO_POSITIONAL_PARAMETERS;
}
}
private static class ChunkedSequence implements TemplateSequenceModel {
private final TemplateSequenceModel wrappedTsm;
private final int chunkSize;
private final TemplateModel fillerItem;
private final int numberOfChunks;
private ChunkedSequence(
TemplateSequenceModel wrappedTsm, int chunkSize, TemplateModel fillerItem)
throws TemplateException {
this.wrappedTsm = wrappedTsm;
this.chunkSize = chunkSize;
this.fillerItem = fillerItem;
numberOfChunks = (wrappedTsm.getCollectionSize() + chunkSize - 1) / chunkSize;
}
@Override
public TemplateModel get(final int chunkIndex)
throws TemplateException {
if (chunkIndex >= numberOfChunks || chunkIndex < 0) {
return null;
}
return new TemplateSequenceModel() {
private final int baseIndex = chunkIndex * chunkSize;
@Override
public TemplateModel get(int relIndex) throws TemplateException {
if (relIndex < 0) {
return null;
}
int absIndex = baseIndex + relIndex;
if (absIndex < wrappedTsm.getCollectionSize()) {
return wrappedTsm.get(absIndex);
} else {
return absIndex < numberOfChunks * chunkSize ? fillerItem : null;
}
}
@Override
public int getCollectionSize() throws TemplateException {
return fillerItem != null || chunkIndex + 1 < numberOfChunks
? chunkSize
: wrappedTsm.getCollectionSize() - baseIndex;
}
@Override
public boolean isEmptyCollection() throws TemplateException {
return getCollectionSize() == 0;
}
@Override
public TemplateModelIterator iterator() throws TemplateException {
return new SequenceTemplateModelIterator(this);
}
};
}
@Override
public int getCollectionSize() throws TemplateException {
return numberOfChunks;
}
@Override
public boolean isEmptyCollection() throws TemplateException {
return numberOfChunks == 0;
}
@Override
public TemplateModelIterator iterator() throws TemplateException {
return new SequenceTemplateModelIterator(this);
}
}
@Override
TemplateModel calculateResult(TemplateSequenceModel tsm) throws TemplateException {
return new BIMethod(tsm);
}
}
static class firstBI extends BuiltInForIterable {
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
TemplateModelIterator iter = model.iterator();
if (!iter.hasNext()) {
return null;
}
return iter.next();
}
}
static class joinBI extends BuiltInForIterable {
private class BIMethodForIterable extends BuiltInCallableImpl implements TemplateFunctionModel {
private final TemplateIterableModel iterable;
private BIMethodForIterable(TemplateIterableModel iterable) {
this.iterable = iterable;
}
@Override
public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env)
throws TemplateException {
final String separator = getStringArgument(args, 0, this);
final String whenEmpty = getOptionalStringArgument(args, 1, this);
final String afterLast = getOptionalStringArgument(args, 2, this);
StringBuilder sb = new StringBuilder();
TemplateModelIterator it = iterable.iterator();
int idx = 0;
boolean hadItem = false;
while (it.hasNext()) {
TemplateModel item = it.next();
if (item != null) {
if (hadItem) {
sb.append(separator);
} else {
hadItem = true;
}
try {
sb.append(_EvalUtils.coerceModelToPlainTextOrUnsupportedMarkup(item, null, null, env));
} catch (TemplateException e) {
throw new TemplateException(e,
"\"?", key, "\" failed at index ", idx, " with this error:\n\n",
MessageUtils.EMBEDDED_MESSAGE_BEGIN,
new _DelayedGetMessageWithoutStackTop(e),
MessageUtils.EMBEDDED_MESSAGE_END);
}
}
idx++;
}
if (hadItem) {
if (afterLast != null) sb.append(afterLast);
} else {
if (whenEmpty != null) sb.append(whenEmpty);
}
return new SimpleString(sb.toString());
}
@Override
public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
return ArgumentArrayLayout.THREE_POSITIONAL_PARAMETERS;
}
}
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
checkNotRightUnboundedNumericalRange(model);
return new BIMethodForIterable(model);
}
}
static class lastBI extends BuiltInForSequence {
@Override
TemplateModel calculateResult(TemplateSequenceModel tsm)
throws TemplateException {
int size = tsm.getCollectionSize();
if (size == 0) {
return null;
}
return tsm.get(size - 1);
}
}
static class reverseBI extends BuiltInForSequence {
private static class ReverseSequence implements TemplateSequenceModel {
private final TemplateSequenceModel seq;
ReverseSequence(TemplateSequenceModel seq) {
this.seq = seq;
}
@Override
public TemplateModel get(int index) throws TemplateException {
return seq.get(seq.getCollectionSize() - 1 - index);
}
@Override
public int getCollectionSize() throws TemplateException {
return seq.getCollectionSize();
}
@Override
public boolean isEmptyCollection() throws TemplateException {
return seq.isEmptyCollection();
}
@Override
public TemplateModelIterator iterator() throws TemplateException {
return new SequenceTemplateModelIterator(this);
}
}
@Override
TemplateModel calculateResult(TemplateSequenceModel tsm) {
if (tsm instanceof ReverseSequence) {
return ((ReverseSequence) tsm).seq;
} else {
return new ReverseSequence(tsm);
}
}
}
static class seq_containsBI extends BuiltInForIterable {
private class BIMethod extends BuiltInCallableImpl implements TemplateFunctionModel {
private TemplateIterableModel iterable;
private BIMethod(TemplateIterableModel coll) {
iterable = coll;
}
@Override
public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env)
throws TemplateException {
TemplateModel arg = args[0];
TemplateModelIterator it = iterable.iterator();
int idx = 0;
while (it.hasNext()) {
if (modelsEqual(idx, it.next(), arg, env)) {
return TemplateBooleanModel.TRUE;
}
idx++;
}
return TemplateBooleanModel.FALSE;
}
@Override
public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
return ArgumentArrayLayout.SINGLE_POSITIONAL_PARAMETER;
}
}
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
return new BIMethod(model);
}
}
static class seq_index_ofBI extends BuiltInForIterable {
private class BIMethod extends BuiltInCallableImpl implements TemplateFunctionModel {
final TemplateIterableModel iterable;
private BIMethod(TemplateIterableModel iterable)
throws TemplateException {
this.iterable = iterable;
}
@Override
public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env)
throws TemplateException {
TemplateModel searched = args[0];
Integer startIndex = getOptionalIntArgument(args, 1, this);
int foundAtIdx;
if (startIndex != null) {
foundAtIdx = iterable instanceof TemplateSequenceModel
? findInSeq(searched, (TemplateSequenceModel) iterable, startIndex, env)
: findInIter(searched, startIndex, env);
} else {
foundAtIdx = iterable instanceof TemplateSequenceModel
? findInSeq(searched, (TemplateSequenceModel) iterable, env)
: findInIter(searched, env);
}
return foundAtIdx == -1 ? TemplateNumberModel.MINUS_ONE : new SimpleNumber(foundAtIdx);
}
@Override
public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
return ArgumentArrayLayout.TWO_POSITIONAL_PARAMETERS;
}
int findInIter(TemplateModel searched, Environment env) throws TemplateException {
return findInIter(searched, 0, Integer.MAX_VALUE, env);
}
int findInIter(TemplateModel searched, int startIndex, Environment env)
throws TemplateException {
if (findFirst) {
return findInIter(searched, startIndex, Integer.MAX_VALUE, env);
} else {
return findInIter(searched, 0, startIndex, env);
}
}
int findInIter(TemplateModel searched,
final int allowedRangeStart, final int allowedRangeEnd, Environment env)
throws TemplateException {
if (allowedRangeEnd < 0) return -1;
TemplateModelIterator it = iterable.iterator();
int foundAtIdx = -1; // -1 is the return value for "not found"
int idx = 0;
searchItem: while (it.hasNext()) {
if (idx > allowedRangeEnd) {
break searchItem;
}
TemplateModel current = it.next();
if (idx >= allowedRangeStart) {
if (modelsEqual(idx, current, searched, env)) {
foundAtIdx = idx;
// Don't stop if it's "find last".
if (findFirst) {
break searchItem;
}
}
}
idx++;
}
return foundAtIdx;
}
int findInSeq(TemplateModel searched, TemplateSequenceModel seq, Environment env)
throws TemplateException {
final int seqSize = seq.getCollectionSize();
final int actualStartIndex;
if (findFirst) {
actualStartIndex = 0;
} else {
actualStartIndex = seqSize - 1;
}
return findInSeq(searched, seq, actualStartIndex, seqSize, env);
}
private int findInSeq(
TemplateModel searched, TemplateSequenceModel seq, int startIndex, Environment env)
throws TemplateException {
int seqSize = seq.getCollectionSize();
if (findFirst) {
if (startIndex >= seqSize) {
return -1;
}
if (startIndex < 0) {
startIndex = 0;
}
} else {
if (startIndex >= seqSize) {
startIndex = seqSize - 1;
}
if (startIndex < 0) {
return -1;
}
}
return findInSeq(searched, seq, startIndex, seqSize, env);
}
private int findInSeq(
TemplateModel searched, TemplateSequenceModel seq, int scanStartIndex, int seqSize, Environment env)
throws TemplateException {
if (findFirst) {
for (int i = scanStartIndex; i < seqSize; i++) {
if (modelsEqual(i, seq.get(i), searched, env)) return i;
}
} else {
for (int i = scanStartIndex; i >= 0; i--) {
if (modelsEqual(i, seq.get(i), searched, env)) return i;
}
}
return -1;
}
}
private boolean findFirst;
seq_index_ofBI(boolean findFirst) {
this.findFirst = findFirst;
}
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
return new BIMethod(model);
}
}
static class sort_byBI extends sortBI {
class BIMethod extends BuiltInCallableImpl implements TemplateFunctionModel {
TemplateSequenceModel seq;
BIMethod(TemplateSequenceModel seq) {
this.seq = seq;
}
@Override
public TemplateModel execute(
TemplateModel[] args, CallPlace callPlace, Environment env)
throws TemplateException {
String[] subvars;
TemplateModel obj = args[0];
if (obj instanceof TemplateStringModel) {
subvars = new String[] { ((TemplateStringModel) obj).getAsString() };
} else if (obj instanceof TemplateSequenceModel) {
TemplateSequenceModel seq = (TemplateSequenceModel) obj;
int ln = seq.getCollectionSize();
subvars = new String[ln];
TemplateModelIterator iter = seq.iterator();
for (int i = 0; i < ln; i++) {
TemplateModel item = iter.next();
if (!(item instanceof TemplateStringModel)) {
throw new TemplateException(
"The argument to ?", key, "(key), when it's a sequence, must be a "
+ "sequence of strings, but the item at index ", i,
" is not a string.");
}
subvars[i] = ((TemplateStringModel) item).getAsString();
}
} else {
throw new TemplateException(
"The argument to ?", key, "(key) must be a string (the name of the subvariable), or a "
+ "sequence of strings (the \"path\" to the subvariable).");
}
return sort(seq, subvars);
}
@Override
public ArgumentArrayLayout getFunctionArgumentArrayLayout() {
return ArgumentArrayLayout.SINGLE_POSITIONAL_PARAMETER;
}
}
@Override
TemplateModel calculateResult(TemplateSequenceModel seq) {
return new BIMethod(seq);
}
}
static class sortBI extends BuiltInForSequence {
private static class BooleanKVPComparator implements Comparator, Serializable {
@Override
public int compare(Object arg0, Object arg1) {
// JDK 1.2 doesn't have Boolean.compareTo
boolean b0 = ((Boolean) ((KVP) arg0).key).booleanValue();
boolean b1 = ((Boolean) ((KVP) arg1).key).booleanValue();
if (b0) {
return b1 ? 0 : 1;
} else {
return b1 ? -1 : 0;
}
}
}
private static class DateKVPComparator implements Comparator, Serializable {
@Override
public int compare(Object arg0, Object arg1) {
return ((Date) ((KVP) arg0).key).compareTo(
(Date) ((KVP) arg1).key);
}
}
/**
* Stores a key-value pair.
*/
private static class KVP {
private Object key;
private Object value;
private KVP(Object key, Object value) {
this.key = key;
this.value = value;
}
}
private static class LexicalKVPComparator implements Comparator {
private Collator collator;
LexicalKVPComparator(Collator collator) {
this.collator = collator;
}
@Override
public int compare(Object arg0, Object arg1) {
return collator.compare(
((KVP) arg0).key, ((KVP) arg1).key);
}
}
private static class NumericalKVPComparator implements Comparator {
private ArithmeticEngine ae;
private NumericalKVPComparator(ArithmeticEngine ae) {
this.ae = ae;
}
@Override
public int compare(Object arg0, Object arg1) {
try {
return ae.compareNumbers(
(Number) ((KVP) arg0).key,
(Number) ((KVP) arg1).key);
} catch (TemplateException e) {
throw new ClassCastException(
"Failed to compare numbers: " + e);
}
}
}
static TemplateException newInconsistentSortKeyTypeException(
int keyNamesLn, String firstType, String firstTypePlural, int index, TemplateModel key) {
String valueInMsg;
String valuesInMsg;
if (keyNamesLn == 0) {
valueInMsg = "value";
valuesInMsg = "values";
} else {
valueInMsg = "key value";
valuesInMsg = "key values";
}
return new TemplateException(
startErrorMessage(keyNamesLn, index),
"All ", valuesInMsg, " in the sequence must be ",
firstTypePlural, ", because the first ", valueInMsg,
" was that. However, the ", valueInMsg,
" of the current item isn't a ", firstType, " but a ",
new _DelayedTemplateLanguageTypeDescription(key), ".");
}
/**
* Sorts a sequence for the {@code sort} and {@code sort_by}
* built-ins.
*
* @param seq the sequence to sort.
* @param keyNames the name of the subvariable whose value is used for the
* sorting. If the sorting is done by a sub-subvaruable, then this
* will be of length 2, and so on. If the sorting is done by the
* sequene items directly, then this argument has to be 0 length
* array or <code>null</code>.
* @return a new sorted sequence, or the original sequence if the
* sequence length was 0.
*/
static TemplateSequenceModel sort(TemplateSequenceModel seq, String[] keyNames)
throws TemplateException {
int ln = seq.getCollectionSize();
if (ln == 0) return seq;
ArrayList res = new ArrayList(ln);
int keyNamesLn = keyNames == null ? 0 : keyNames.length;
// Copy the Seq into a Java List[KVP] (also detects key type at the 1st item):
int keyType = KEY_TYPE_NOT_YET_DETECTED;
Comparator keyComparator = null;
TemplateModelIterator iter = seq.iterator();
for (int i = 0; i < ln; i++) {
final TemplateModel item = iter.next();
TemplateModel key = item;
for (int keyNameI = 0; keyNameI < keyNamesLn; keyNameI++) {
try {
key = ((TemplateHashModel) key).get(keyNames[keyNameI]);
} catch (ClassCastException e) {
if (!(key instanceof TemplateHashModel)) {
throw new TemplateException(
startErrorMessage(keyNamesLn, i),
(keyNameI == 0
? "Sequence items must be hashes when using ?sortBy. "
: "The " + _StringUtils.jQuote(keyNames[keyNameI - 1])),
" subvariable is not a hash, so ?sortBy ",
"can't proceed with getting the ",
new _DelayedJQuote(keyNames[keyNameI]),
" subvariable.");
} else {
throw e;
}
}
if (key == null) {
throw new TemplateException(
startErrorMessage(keyNamesLn, i),
"The " + _StringUtils.jQuote(keyNames[keyNameI]), " subvariable was null or missing.");
}
} // for each key
if (keyType == KEY_TYPE_NOT_YET_DETECTED) {
if (key instanceof TemplateStringModel) {
keyType = KEY_TYPE_STRING;
keyComparator = new LexicalKVPComparator(
Environment.getCurrentEnvironment().getCollator());
} else if (key instanceof TemplateNumberModel) {
keyType = KEY_TYPE_NUMBER;
keyComparator = new NumericalKVPComparator(
Environment.getCurrentEnvironment().getArithmeticEngine());
} else if (key instanceof TemplateDateModel) {
keyType = KEY_TYPE_DATE;
keyComparator = new DateKVPComparator();
} else if (key instanceof TemplateBooleanModel) {
keyType = KEY_TYPE_BOOLEAN;
keyComparator = new BooleanKVPComparator();
} else {
throw new TemplateException(
startErrorMessage(keyNamesLn, i),
"Values used for sorting must be numbers, strings, date/times or booleans.");
}
}
switch(keyType) {
case KEY_TYPE_STRING:
try {
res.add(new KVP(
((TemplateStringModel) key).getAsString(),
item));
} catch (ClassCastException e) {
if (!(key instanceof TemplateStringModel)) {
throw newInconsistentSortKeyTypeException(
keyNamesLn, "string", "strings", i, key);
} else {
throw e;
}
}
break;
case KEY_TYPE_NUMBER:
try {
res.add(new KVP(
((TemplateNumberModel) key).getAsNumber(),
item));
} catch (ClassCastException e) {
if (!(key instanceof TemplateNumberModel)) {
throw newInconsistentSortKeyTypeException(
keyNamesLn, "number", "numbers", i, key);
}
}
break;
case KEY_TYPE_DATE:
try {
res.add(new KVP(
((TemplateDateModel) key).getAsDate(),
item));
} catch (ClassCastException e) {
if (!(key instanceof TemplateDateModel)) {
throw newInconsistentSortKeyTypeException(
keyNamesLn, "date/time", "date/times", i, key);
}
}
break;
case KEY_TYPE_BOOLEAN:
try {
res.add(new KVP(
Boolean.valueOf(((TemplateBooleanModel) key).getAsBoolean()),
item));
} catch (ClassCastException e) {
if (!(key instanceof TemplateBooleanModel)) {
throw newInconsistentSortKeyTypeException(
keyNamesLn, "boolean", "booleans", i, key);
}
}
break;
default:
throw new BugException("Unexpected key type");
}
}
// Sort tje List[KVP]:
try {
Collections.sort(res, keyComparator);
} catch (Exception exc) {
throw new TemplateException(exc,
startErrorMessage(keyNamesLn), "Unexpected error while sorting:" + exc);
}
// Convert the List[KVP] to List[V]:
for (int i = 0; i < ln; i++) {
res.set(i, ((KVP) res.get(i)).value);
}
return new TemplateModelListSequence(res);
}
static Object[] startErrorMessage(int keyNamesLn) {
return new Object[] { (keyNamesLn == 0 ? "?sort" : "?sortBy(...)"), " failed: " };
}
static Object[] startErrorMessage(int keyNamesLn, int index) {
return new Object[] {
(keyNamesLn == 0 ? "?sort" : "?sortBy(...)"),
" failed at sequence index ", Integer.valueOf(index),
(index == 0 ? ": " : " (0-based): ") };
}
static final int KEY_TYPE_NOT_YET_DETECTED = 0;
static final int KEY_TYPE_STRING = 1;
static final int KEY_TYPE_NUMBER = 2;
static final int KEY_TYPE_DATE = 3;
static final int KEY_TYPE_BOOLEAN = 4;
@Override
TemplateModel calculateResult(TemplateSequenceModel seq)
throws TemplateException {
return sort(seq, null);
}
}
private static void checkNotRightUnboundedNumericalRange(TemplateModel model) throws TemplateException {
if (model instanceof RightUnboundedRangeModel) {
throw new TemplateException(
"The input sequence is a right-unbounded numerical range, thus, it's infinitely long, and can't " +
"processed with this built-in.");
}
}
private static boolean modelsEqual(
int seqItemIndex, TemplateModel seqItem, TemplateModel searchedItem, Environment env)
throws TemplateException {
try {
return _EvalUtils.compare(
seqItem, null,
_EvalUtils.CMP_OP_EQUALS, null,
searchedItem, null,
null, false,
true, true, true, // TODO [FM3] The last one is true to emulate an old bug for BC
env);
} catch (TemplateException ex) {
throw new TemplateException(ex,
"This error has occurred when comparing sequence item at 0-based index ", Integer.valueOf(seqItemIndex),
" to the searched item:\n", new _DelayedGetMessage(ex));
}
}
static class sequenceBI extends BuiltInForIterable {
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
if (model instanceof TemplateSequenceModel) {
return model;
}
NativeSequence seq =
model instanceof TemplateCollectionModel
? new NativeSequence(((TemplateCollectionModel) model).getCollectionSize())
: new NativeSequence();
for (TemplateModelIterator iter = model.iterator(); iter.hasNext(); ) {
seq.add(iter.next());
}
return seq;
}
}
private static abstract class MinOrMaxBI extends BuiltInForIterable {
private final int comparatorOperator;
protected MinOrMaxBI(int comparatorOperator) {
this.comparatorOperator = comparatorOperator;
}
@Override
TemplateModel calculateResult(TemplateIterableModel model, Environment env) throws TemplateException {
checkNotRightUnboundedNumericalRange(model);
TemplateModel best = null;
TemplateModelIterator iter = model.iterator();
while (iter.hasNext()) {
TemplateModel cur = iter.next();
if (cur != null
&& (best == null || _EvalUtils.compare(cur, null, comparatorOperator, null, best,
null, this, true, false, false, false, env))) {
best = cur;
}
}
return best;
}
}
static class maxBI extends MinOrMaxBI {
public maxBI() {
super(_EvalUtils.CMP_OP_GREATER_THAN);
}
}
static class minBI extends MinOrMaxBI {
public minBI() {
super(_EvalUtils.CMP_OP_LESS_THAN);
}
}
// Can't be instantiated
private BuiltInsForSequences() { }
}