http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java new file mode 100644 index 0000000..814b362 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java @@ -0,0 +1,871 @@ +/* + * 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 java.io.Serializable; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.model.Constants; +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateCollectionModel; +import org.apache.freemarker.core.model.TemplateDateModel; +import org.apache.freemarker.core.model.TemplateHashModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateModelIterator; +import org.apache.freemarker.core.model.TemplateNumberModel; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.impl.CollectionAndSequence; +import org.apache.freemarker.core.model.impl.SimpleNumber; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.model.impl.TemplateModelListSequence; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * A holder for builtins that operate exclusively on sequence or collection left-hand value. + */ +class BuiltInsForSequences { + + static class chunkBI extends BuiltInForSequence { + + private class BIMethod implements TemplateMethodModelEx { + + private final TemplateSequenceModel tsm; + + private BIMethod(TemplateSequenceModel tsm) { + this.tsm = tsm; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1, 2); + int chunkSize = getNumberMethodArg(args, 0).intValue(); + + return new ChunkedSequence( + tsm, + chunkSize, + args.size() > 1 ? (TemplateModel) args.get(1) : null); + } + } + + 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 TemplateModelException { + if (chunkSize < 1) { + throw new _TemplateModelException("The 1st argument to ?', key, ' (...) must be at least 1."); + } + this.wrappedTsm = wrappedTsm; + this.chunkSize = chunkSize; + this.fillerItem = fillerItem; + numberOfChunks = (wrappedTsm.size() + chunkSize - 1) / chunkSize; + } + + @Override + public TemplateModel get(final int chunkIndex) + throws TemplateModelException { + if (chunkIndex >= numberOfChunks) { + return null; + } + + return new TemplateSequenceModel() { + + private final int baseIndex = chunkIndex * chunkSize; + + @Override + public TemplateModel get(int relIndex) + throws TemplateModelException { + int absIndex = baseIndex + relIndex; + if (absIndex < wrappedTsm.size()) { + return wrappedTsm.get(absIndex); + } else { + return absIndex < numberOfChunks * chunkSize + ? fillerItem + : null; + } + } + + @Override + public int size() throws TemplateModelException { + return fillerItem != null || chunkIndex + 1 < numberOfChunks + ? chunkSize + : wrappedTsm.size() - baseIndex; + } + + }; + } + + @Override + public int size() throws TemplateModelException { + return numberOfChunks; + } + + } + + @Override + TemplateModel calculateResult(TemplateSequenceModel tsm) throws TemplateModelException { + return new BIMethod(tsm); + } + + } + + static class firstBI extends ASTExpBuiltIn { + + @Override + TemplateModel _eval(Environment env) + throws TemplateException { + TemplateModel model = target.eval(env); + // In 2.3.x only, we prefer TemplateSequenceModel for + // backward compatibility. In 2.4.x, we prefer TemplateCollectionModel. + if (model instanceof TemplateSequenceModel) { + return calculateResultForSequence((TemplateSequenceModel) model); + } else if (model instanceof TemplateCollectionModel) { + return calculateResultForColletion((TemplateCollectionModel) model); + } else { + throw new NonSequenceOrCollectionException(target, model, env); + } + } + + private TemplateModel calculateResultForSequence(TemplateSequenceModel seq) + throws TemplateModelException { + if (seq.size() == 0) { + return null; + } + return seq.get(0); + } + + private TemplateModel calculateResultForColletion(TemplateCollectionModel coll) + throws TemplateModelException { + TemplateModelIterator iter = coll.iterator(); + if (!iter.hasNext()) { + return null; + } + return iter.next(); + } + + } + + static class joinBI extends ASTExpBuiltIn { + + private class BIMethodForCollection implements TemplateMethodModelEx { + + private final Environment env; + private final TemplateCollectionModel coll; + + private BIMethodForCollection(Environment env, TemplateCollectionModel coll) { + this.env = env; + this.coll = coll; + } + + @Override + public Object exec(List args) + throws TemplateModelException { + checkMethodArgCount(args, 1, 3); + final String separator = getStringMethodArg(args, 0); + final String whenEmpty = getOptStringMethodArg(args, 1); + final String afterLast = getOptStringMethodArg(args, 2); + + StringBuilder sb = new StringBuilder(); + + TemplateModelIterator it = coll.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(_EvalUtil.coerceModelToStringOrUnsupportedMarkup(item, null, null, env)); + } catch (TemplateException e) { + throw new _TemplateModelException(e, + "\"?", key, "\" failed at index ", Integer.valueOf(idx), " with this error:\n\n", + MessageUtil.EMBEDDED_MESSAGE_BEGIN, + new _DelayedGetMessageWithoutStackTop(e), + MessageUtil.EMBEDDED_MESSAGE_END); + } + } + idx++; + } + if (hadItem) { + if (afterLast != null) sb.append(afterLast); + } else { + if (whenEmpty != null) sb.append(whenEmpty); + } + return new SimpleScalar(sb.toString()); + } + + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + TemplateModel model = target.eval(env); + if (model instanceof TemplateCollectionModel) { + if (model instanceof RightUnboundedRangeModel) { + throw new _TemplateModelException( + "The sequence to join was right-unbounded numerical range, thus it's infinitely long."); + } + return new BIMethodForCollection(env, (TemplateCollectionModel) model); + } else if (model instanceof TemplateSequenceModel) { + return new BIMethodForCollection(env, new CollectionAndSequence((TemplateSequenceModel) model)); + } else { + throw new NonSequenceOrCollectionException(target, model, env); + } + } + + } + + static class lastBI extends BuiltInForSequence { + @Override + TemplateModel calculateResult(TemplateSequenceModel tsm) + throws TemplateModelException { + if (tsm.size() == 0) { + return null; + } + return tsm.get(tsm.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 TemplateModelException { + return seq.get(seq.size() - 1 - index); + } + + @Override + public int size() throws TemplateModelException { + return seq.size(); + } + } + + @Override + TemplateModel calculateResult(TemplateSequenceModel tsm) { + if (tsm instanceof ReverseSequence) { + return ((ReverseSequence) tsm).seq; + } else { + return new ReverseSequence(tsm); + } + } + } + + static class seq_containsBI extends ASTExpBuiltIn { + private class BIMethodForCollection implements TemplateMethodModelEx { + private TemplateCollectionModel m_coll; + private Environment m_env; + + private BIMethodForCollection(TemplateCollectionModel coll, Environment env) { + m_coll = coll; + m_env = env; + } + + @Override + public Object exec(List args) + throws TemplateModelException { + checkMethodArgCount(args, 1); + TemplateModel arg = (TemplateModel) args.get(0); + TemplateModelIterator it = m_coll.iterator(); + int idx = 0; + while (it.hasNext()) { + if (modelsEqual(idx, it.next(), arg, m_env)) + return TemplateBooleanModel.TRUE; + idx++; + } + return TemplateBooleanModel.FALSE; + } + + } + + private class BIMethodForSequence implements TemplateMethodModelEx { + private TemplateSequenceModel m_seq; + private Environment m_env; + + private BIMethodForSequence(TemplateSequenceModel seq, Environment env) { + m_seq = seq; + m_env = env; + } + + @Override + public Object exec(List args) + throws TemplateModelException { + checkMethodArgCount(args, 1); + TemplateModel arg = (TemplateModel) args.get(0); + int size = m_seq.size(); + for (int i = 0; i < size; i++) { + if (modelsEqual(i, m_seq.get(i), arg, m_env)) + return TemplateBooleanModel.TRUE; + } + return TemplateBooleanModel.FALSE; + } + + } + + @Override + TemplateModel _eval(Environment env) + throws TemplateException { + TemplateModel model = target.eval(env); + // In 2.3.x only, we prefer TemplateSequenceModel for + // backward compatibility. In 2.4.x, we prefer TemplateCollectionModel. + if (model instanceof TemplateSequenceModel) { + return new BIMethodForSequence((TemplateSequenceModel) model, env); + } else if (model instanceof TemplateCollectionModel) { + return new BIMethodForCollection((TemplateCollectionModel) model, env); + } else { + throw new NonSequenceOrCollectionException(target, model, env); + } + } + + } + + static class seq_index_ofBI extends ASTExpBuiltIn { + + private class BIMethod implements TemplateMethodModelEx { + + protected final TemplateSequenceModel m_seq; + protected final TemplateCollectionModel m_col; + protected final Environment m_env; + + private BIMethod(Environment env) + throws TemplateException { + TemplateModel model = target.eval(env); + m_seq = model instanceof TemplateSequenceModel + ? (TemplateSequenceModel) model + : null; + // [FM3] Rework the below + // In 2.3.x only, we deny the possibility of collection + // access if there's sequence access. This is so to minimize + // the change of compatibility issues; without this, objects + // that implement both the sequence and collection interfaces + // would suddenly start using the collection interface, and if + // that's buggy that would surface now, breaking the application + // that despite its bugs has worked earlier. + m_col = m_seq == null && model instanceof TemplateCollectionModel + ? (TemplateCollectionModel) model + : null; + if (m_seq == null && m_col == null) { + throw new NonSequenceOrCollectionException(target, model, env); + } + + m_env = env; + } + + @Override + public final Object exec(List args) + throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + + TemplateModel target = (TemplateModel) args.get(0); + int foundAtIdx; + if (argCnt > 1) { + int startIndex = getNumberMethodArg(args, 1).intValue(); + // In 2.3.x only, we prefer TemplateSequenceModel for + // backward compatibility: + foundAtIdx = m_seq != null + ? findInSeq(target, startIndex) + : findInCol(target, startIndex); + } else { + // In 2.3.x only, we prefer TemplateSequenceModel for + // backward compatibility: + foundAtIdx = m_seq != null + ? findInSeq(target) + : findInCol(target); + } + return foundAtIdx == -1 ? Constants.MINUS_ONE : new SimpleNumber(foundAtIdx); + } + + int findInCol(TemplateModel target) throws TemplateModelException { + return findInCol(target, 0, Integer.MAX_VALUE); + } + + protected int findInCol(TemplateModel target, int startIndex) + throws TemplateModelException { + if (m_dir == 1) { + return findInCol(target, startIndex, Integer.MAX_VALUE); + } else { + return findInCol(target, 0, startIndex); + } + } + + protected int findInCol(TemplateModel target, + final int allowedRangeStart, final int allowedRangeEnd) + throws TemplateModelException { + if (allowedRangeEnd < 0) return -1; + + TemplateModelIterator it = m_col.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, target, m_env)) { + foundAtIdx = idx; + if (m_dir == 1) break searchItem; // "find first" + // Otherwise it's "find last". + } + } + idx++; + } + return foundAtIdx; + } + + int findInSeq(TemplateModel target) + throws TemplateModelException { + final int seqSize = m_seq.size(); + final int actualStartIndex; + + if (m_dir == 1) { + actualStartIndex = 0; + } else { + actualStartIndex = seqSize - 1; + } + + return findInSeq(target, actualStartIndex, seqSize); + } + + private int findInSeq(TemplateModel target, int startIndex) + throws TemplateModelException { + int seqSize = m_seq.size(); + + if (m_dir == 1) { + if (startIndex >= seqSize) { + return -1; + } + if (startIndex < 0) { + startIndex = 0; + } + } else { + if (startIndex >= seqSize) { + startIndex = seqSize - 1; + } + if (startIndex < 0) { + return -1; + } + } + + return findInSeq(target, startIndex, seqSize); + } + + private int findInSeq( + TemplateModel target, int scanStartIndex, int seqSize) + throws TemplateModelException { + if (m_dir == 1) { + for (int i = scanStartIndex; i < seqSize; i++) { + if (modelsEqual(i, m_seq.get(i), target, m_env)) return i; + } + } else { + for (int i = scanStartIndex; i >= 0; i--) { + if (modelsEqual(i, m_seq.get(i), target, m_env)) return i; + } + } + return -1; + } + + } + + private int m_dir; + + seq_index_ofBI(int dir) { + m_dir = dir; + } + + @Override + TemplateModel _eval(Environment env) + throws TemplateException { + return new BIMethod(env); + } + } + + static class sort_byBI extends sortBI { + class BIMethod implements TemplateMethodModelEx { + TemplateSequenceModel seq; + + BIMethod(TemplateSequenceModel seq) { + this.seq = seq; + } + + @Override + public Object exec(List args) + throws TemplateModelException { + // Should be: + // checkMethodArgCount(args, 1); + // But for BC: + if (args.size() < 1) throw MessageUtil.newArgCntError("?" + key, args.size(), 1); + + String[] subvars; + Object obj = args.get(0); + if (obj instanceof TemplateScalarModel) { + subvars = new String[]{((TemplateScalarModel) obj).getAsString()}; + } else if (obj instanceof TemplateSequenceModel) { + TemplateSequenceModel seq = (TemplateSequenceModel) obj; + int ln = seq.size(); + subvars = new String[ln]; + for (int i = 0; i < ln; i++) { + Object item = seq.get(i); + try { + subvars[i] = ((TemplateScalarModel) item) + .getAsString(); + } catch (ClassCastException e) { + if (!(item instanceof TemplateScalarModel)) { + throw new _TemplateModelException( + "The argument to ?", key, "(key), when it's a sequence, must be a " + + "sequence of strings, but the item at index ", Integer.valueOf(i), + " is not a string."); + } + } + } + } else { + throw new _TemplateModelException( + "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 + 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 TemplateModelException 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 _TemplateModelException( + 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 _DelayedFTLTypeDescription(key), "."); + } + + /** + * Sorts a sequence for the <tt>sort</tt> and <tt>sort_by</tt> + * 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 TemplateModelException { + int ln = seq.size(); + 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; + for (int i = 0; i < ln; i++) { + final TemplateModel item = seq.get(i); + 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 _TemplateModelException( + startErrorMessage(keyNamesLn, i), + (keyNameI == 0 + ? "Sequence items must be hashes when using ?sort_by. " + : "The " + _StringUtil.jQuote(keyNames[keyNameI - 1])), + " subvariable is not a hash, so ?sort_by ", + "can't proceed with getting the ", + new _DelayedJQuote(keyNames[keyNameI]), + " subvariable."); + } else { + throw e; + } + } + if (key == null) { + throw new _TemplateModelException( + startErrorMessage(keyNamesLn, i), + "The " + _StringUtil.jQuote(keyNames[keyNameI]), " subvariable was null or missing."); + } + } // for each key + + if (keyType == KEY_TYPE_NOT_YET_DETECTED) { + if (key instanceof TemplateScalarModel) { + 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 _TemplateModelException( + 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( + ((TemplateScalarModel) key).getAsString(), + item)); + } catch (ClassCastException e) { + if (!(key instanceof TemplateScalarModel)) { + 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 _TemplateModelException(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" : "?sort_by(...)"), " failed: " }; + } + + static Object[] startErrorMessage(int keyNamesLn, int index) { + return new Object[] { + (keyNamesLn == 0 ? "?sort" : "?sort_by(...)"), + " 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 TemplateModelException { + return sort(seq, null); + } + + } + + private static boolean modelsEqual( + int seqItemIndex, TemplateModel seqItem, TemplateModel searchedItem, + Environment env) + throws TemplateModelException { + try { + return _EvalUtil.compare( + seqItem, null, + _EvalUtil.CMP_OP_EQUALS, null, + searchedItem, null, + null, false, + true, true, true, // The last one is true to emulate an old bug for BC + env); + } catch (TemplateException ex) { + throw new _TemplateModelException(ex, + "This error has occurred when comparing sequence item at 0-based index ", Integer.valueOf(seqItemIndex), + " to the searched item:\n", new _DelayedGetMessage(ex)); + } + } + + // Can't be instantiated + private BuiltInsForSequences() { } + +} \ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java new file mode 100644 index 0000000..bcf00c4 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java @@ -0,0 +1,697 @@ +/* + * 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 java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateMethodModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.impl.SimpleNumber; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util._StringUtil; + +class BuiltInsForStringsBasic { + + static class cap_firstBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + int i = 0; + int ln = s.length(); + while (i < ln && Character.isWhitespace(s.charAt(i))) { + i++; + } + if (i < ln) { + StringBuilder b = new StringBuilder(s); + b.setCharAt(i, Character.toUpperCase(s.charAt(i))); + s = b.toString(); + } + return new SimpleScalar(s); + } + } + + static class capitalizeBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.capitalize(s)); + } + } + + static class chop_linebreakBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.chomp(s)); + } + } + + static class containsBI extends ASTExpBuiltIn { + + private class BIMethod implements TemplateMethodModelEx { + + private final String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + return s.indexOf(getStringMethodArg(args, 0)) != -1 + ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return new BIMethod(target.evalAndCoerceToStringOrUnsupportedMarkup(env, + "For sequences/collections (lists and such) use \"?seq_contains\" instead.")); + } + } + + static class ends_withBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + return s.endsWith(getStringMethodArg(args, 0)) ? + TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class ensure_ends_withBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + String suffix = getStringMethodArg(args, 0); + return new SimpleScalar(s.endsWith(suffix) ? s : s + suffix); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class ensure_starts_withBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1, 3); + + final String checkedPrefix = getStringMethodArg(args, 0); + + final boolean startsWithPrefix; + final String addedPrefix; + if (args.size() > 1) { + addedPrefix = getStringMethodArg(args, 1); + long flags = args.size() > 2 + ? RegexpHelper.parseFlagString(getStringMethodArg(args, 2)) + : RegexpHelper.RE_FLAG_REGEXP; + + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true); + if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) { + startsWithPrefix = s.startsWith(checkedPrefix); + } else { + startsWithPrefix = s.toLowerCase().startsWith(checkedPrefix.toLowerCase()); + } + } else { + Pattern pattern = RegexpHelper.getPattern(checkedPrefix, (int) flags); + final Matcher matcher = pattern.matcher(s); + startsWithPrefix = matcher.lookingAt(); + } + } else { + startsWithPrefix = s.startsWith(checkedPrefix); + addedPrefix = checkedPrefix; + } + return new SimpleScalar(startsWithPrefix ? s : addedPrefix + s); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class index_ofBI extends ASTExpBuiltIn { + + private class BIMethod implements TemplateMethodModelEx { + + private final String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String subStr = getStringMethodArg(args, 0); + if (argCnt > 1) { + int startIdx = getNumberMethodArg(args, 1).intValue(); + return new SimpleNumber(findLast ? s.lastIndexOf(subStr, startIdx) : s.indexOf(subStr, startIdx)); + } else { + return new SimpleNumber(findLast ? s.lastIndexOf(subStr) : s.indexOf(subStr)); + } + } + } + + private final boolean findLast; + + index_ofBI(boolean findLast) { + this.findLast = findLast; + } + + @Override + TemplateModel _eval(Environment env) throws TemplateException { + return new BIMethod(target.evalAndCoerceToStringOrUnsupportedMarkup(env, + "For sequences/collections (lists and such) use \"?seq_index_of\" instead.")); + } + } + + static class keep_afterBI extends BuiltInForString { + class KeepAfterMethod implements TemplateMethodModelEx { + private String s; + + KeepAfterMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String separatorString = getStringMethodArg(args, 0); + long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0; + + int startIndex; + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true); + if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) { + startIndex = s.indexOf(separatorString); + } else { + startIndex = s.toLowerCase().indexOf(separatorString.toLowerCase()); + } + if (startIndex >= 0) { + startIndex += separatorString.length(); + } + } else { + Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags); + final Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + startIndex = matcher.end(); + } else { + startIndex = -1; + } + } + return startIndex == -1 ? TemplateScalarModel.EMPTY_STRING : new SimpleScalar(s.substring(startIndex)); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateModelException { + return new KeepAfterMethod(s); + } + + } + + static class keep_after_lastBI extends BuiltInForString { + class KeepAfterMethod implements TemplateMethodModelEx { + private String s; + + KeepAfterMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String separatorString = getStringMethodArg(args, 0); + long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0; + + int startIndex; + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true); + if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) { + startIndex = s.lastIndexOf(separatorString); + } else { + startIndex = s.toLowerCase().lastIndexOf(separatorString.toLowerCase()); + } + if (startIndex >= 0) { + startIndex += separatorString.length(); + } + } else { + if (separatorString.length() == 0) { + startIndex = s.length(); + } else { + Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags); + final Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + startIndex = matcher.end(); + while (matcher.find(matcher.start() + 1)) { + startIndex = matcher.end(); + } + } else { + startIndex = -1; + } + } + } + return startIndex == -1 ? TemplateScalarModel.EMPTY_STRING : new SimpleScalar(s.substring(startIndex)); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateModelException { + return new KeepAfterMethod(s); + } + + } + + static class keep_beforeBI extends BuiltInForString { + class KeepUntilMethod implements TemplateMethodModelEx { + private String s; + + KeepUntilMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String separatorString = getStringMethodArg(args, 0); + long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0; + + int stopIndex; + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true); + if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) { + stopIndex = s.indexOf(separatorString); + } else { + stopIndex = s.toLowerCase().indexOf(separatorString.toLowerCase()); + } + } else { + Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags); + final Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + stopIndex = matcher.start(); + } else { + stopIndex = -1; + } + } + return stopIndex == -1 ? new SimpleScalar(s) : new SimpleScalar(s.substring(0, stopIndex)); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateModelException { + return new KeepUntilMethod(s); + } + + } + + // TODO + static class keep_before_lastBI extends BuiltInForString { + class KeepUntilMethod implements TemplateMethodModelEx { + private String s; + + KeepUntilMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String separatorString = getStringMethodArg(args, 0); + long flags = argCnt > 1 ? RegexpHelper.parseFlagString(getStringMethodArg(args, 1)) : 0; + + int stopIndex; + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkOnlyHasNonRegexpFlags(key, flags, true); + if ((flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) == 0) { + stopIndex = s.lastIndexOf(separatorString); + } else { + stopIndex = s.toLowerCase().lastIndexOf(separatorString.toLowerCase()); + } + } else { + if (separatorString.length() == 0) { + stopIndex = s.length(); + } else { + Pattern pattern = RegexpHelper.getPattern(separatorString, (int) flags); + final Matcher matcher = pattern.matcher(s); + if (matcher.find()) { + stopIndex = matcher.start(); + while (matcher.find(stopIndex + 1)) { + stopIndex = matcher.start(); + } + } else { + stopIndex = -1; + } + } + } + return stopIndex == -1 ? new SimpleScalar(s) : new SimpleScalar(s.substring(0, stopIndex)); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateModelException { + return new KeepUntilMethod(s); + } + + } + + static class lengthBI extends BuiltInForString { + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new SimpleNumber(s.length()); + } + + } + + static class lower_caseBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(s.toLowerCase(env.getLocale())); + } + } + + static class padBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + + private final String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + + int width = getNumberMethodArg(args, 0).intValue(); + + if (argCnt > 1) { + String filling = getStringMethodArg(args, 1); + try { + return new SimpleScalar( + leftPadder + ? _StringUtil.leftPad(s, width, filling) + : _StringUtil.rightPad(s, width, filling)); + } catch (IllegalArgumentException e) { + if (filling.length() == 0) { + throw new _TemplateModelException( + "?", key, "(...) argument #2 can't be a 0-length string."); + } else { + throw new _TemplateModelException(e, + "?", key, "(...) failed: ", e); + } + } + } else { + return new SimpleScalar(leftPadder ? _StringUtil.leftPad(s, width) : _StringUtil.rightPad(s, width)); + } + } + } + + private final boolean leftPadder; + + padBI(boolean leftPadder) { + this.leftPadder = leftPadder; + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class remove_beginningBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + String prefix = getStringMethodArg(args, 0); + return new SimpleScalar(s.startsWith(prefix) ? s.substring(prefix.length()) : s); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class remove_endingBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + String suffix = getStringMethodArg(args, 0); + return new SimpleScalar(s.endsWith(suffix) ? s.substring(0, s.length() - suffix.length()) : s); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class split_BI extends BuiltInForString { + class SplitMethod implements TemplateMethodModel { + private String s; + + SplitMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + int argCnt = args.size(); + checkMethodArgCount(argCnt, 1, 2); + String splitString = (String) args.get(0); + long flags = argCnt > 1 ? RegexpHelper.parseFlagString((String) args.get(1)) : 0; + String[] result = null; + if ((flags & RegexpHelper.RE_FLAG_REGEXP) == 0) { + RegexpHelper.checkNonRegexpFlags("split", flags); + result = _StringUtil.split(s, splitString, + (flags & RegexpHelper.RE_FLAG_CASE_INSENSITIVE) != 0); + } else { + Pattern pattern = RegexpHelper.getPattern(splitString, (int) flags); + result = pattern.split(s); + } + return new NativeStringArraySequence(result); + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateModelException { + return new SplitMethod(s); + } + + } + + static class starts_withBI extends BuiltInForString { + + private class BIMethod implements TemplateMethodModelEx { + private String s; + + private BIMethod(String s) { + this.s = s; + } + + @Override + public Object exec(List args) throws TemplateModelException { + checkMethodArgCount(args, 1); + return s.startsWith(getStringMethodArg(args, 0)) ? + TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + } + + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + return new BIMethod(s); + } + } + + static class substringBI extends BuiltInForString { + + @Override + TemplateModel calculateResult(final String s, final Environment env) throws TemplateException { + return new TemplateMethodModelEx() { + + @Override + public Object exec(java.util.List args) throws TemplateModelException { + int argCount = args.size(); + checkMethodArgCount(argCount, 1, 2); + + int beginIdx = getNumberMethodArg(args, 0).intValue(); + + final int len = s.length(); + + if (beginIdx < 0) { + throw newIndexLessThan0Exception(0, beginIdx); + } else if (beginIdx > len) { + throw newIndexGreaterThanLengthException(0, beginIdx, len); + } + + if (argCount > 1) { + int endIdx = getNumberMethodArg(args, 1).intValue(); + if (endIdx < 0) { + throw newIndexLessThan0Exception(1, endIdx); + } else if (endIdx > len) { + throw newIndexGreaterThanLengthException(1, endIdx, len); + } + if (beginIdx > endIdx) { + throw MessageUtil.newMethodArgsInvalidValueException("?" + key, + "The begin index argument, ", Integer.valueOf(beginIdx), + ", shouldn't be greater than the end index argument, ", + Integer.valueOf(endIdx), "."); + } + return new SimpleScalar(s.substring(beginIdx, endIdx)); + } else { + return new SimpleScalar(s.substring(beginIdx)); + } + } + + private TemplateModelException newIndexGreaterThanLengthException( + int argIdx, int idx, final int len) throws TemplateModelException { + return MessageUtil.newMethodArgInvalidValueException( + "?" + key, argIdx, + "The index mustn't be greater than the length of the string, ", + Integer.valueOf(len), + ", but it was ", Integer.valueOf(idx), "."); + } + + private TemplateModelException newIndexLessThan0Exception( + int argIdx, int idx) throws TemplateModelException { + return MessageUtil.newMethodArgInvalidValueException( + "?" + key, argIdx, + "The index must be at least 0, but was ", Integer.valueOf(idx), "."); + } + + }; + } + } + + static class trimBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(s.trim()); + } + } + + static class uncap_firstBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + int i = 0; + int ln = s.length(); + while (i < ln && Character.isWhitespace(s.charAt(i))) { + i++; + } + if (i < ln) { + StringBuilder b = new StringBuilder(s); + b.setCharAt(i, Character.toLowerCase(s.charAt(i))); + s = b.toString(); + } + return new SimpleScalar(s); + } + } + + static class upper_caseBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(s.toUpperCase(env.getLocale())); + } + } + + static class word_listBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + ArrayList<String> result = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) { + result.add(st.nextToken()); + } + return new NativeStringListSequence(result); + } + } + + // Can't be instantiated + private BuiltInsForStringsBasic() { } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java new file mode 100644 index 0000000..80eb9d3 --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java @@ -0,0 +1,195 @@ +/* + * 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 java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.List; + +import org.apache.freemarker.core.model.TemplateMethodModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.impl.SimpleScalar; +import org.apache.freemarker.core.util._StringUtil; + +class BuiltInsForStringsEncoding { + + static class htmlBI extends BuiltInForLegacyEscaping { + + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.XHTMLEnc(s)); + } + + } + + static class j_stringBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.javaStringEnc(s)); + } + } + + static class js_stringBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.javaScriptStringEnc(s)); + } + } + + static class json_stringBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.jsonStringEnc(s)); + } + } + + static class rtfBI extends BuiltInForLegacyEscaping { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.RTFEnc(s)); + } + } + + static class urlBI extends BuiltInForString { + + static class UrlBIResult extends BuiltInsForStringsEncoding.AbstractUrlBIResult { + + protected UrlBIResult(ASTExpBuiltIn parent, String target, Environment env) { + super(parent, target, env); + } + + @Override + protected String encodeWithCharset(Charset charset) throws UnsupportedEncodingException { + return _StringUtil.URLEnc(targetAsString, charset); + } + + } + + @Override + TemplateModel calculateResult(String s, Environment env) { + return new UrlBIResult(this, s, env); + } + + } + + static class urlPathBI extends BuiltInForString { + + static class UrlPathBIResult extends BuiltInsForStringsEncoding.AbstractUrlBIResult { + + protected UrlPathBIResult(ASTExpBuiltIn parent, String target, Environment env) { + super(parent, target, env); + } + + @Override + protected String encodeWithCharset(Charset charset) throws UnsupportedEncodingException { + return _StringUtil.URLPathEnc(targetAsString, charset); + } + + } + + @Override + TemplateModel calculateResult(String s, Environment env) { + return new UrlPathBIResult(this, s, env); + } + + } + + static class xhtmlBI extends BuiltInForLegacyEscaping { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.XHTMLEnc(s)); + } + } + + static class xmlBI extends BuiltInForLegacyEscaping { + @Override + TemplateModel calculateResult(String s, Environment env) { + return new SimpleScalar(_StringUtil.XMLEnc(s)); + } + } + + // Can't be instantiated + private BuiltInsForStringsEncoding() { } + + static abstract class AbstractUrlBIResult implements + TemplateScalarModel, TemplateMethodModel { + + protected final ASTExpBuiltIn parent; + protected final String targetAsString; + private final Environment env; + private String cachedResult; + + protected AbstractUrlBIResult(ASTExpBuiltIn parent, String targetAsString, Environment env) { + this.parent = parent; + this.targetAsString = targetAsString; + this.env = env; + } + + protected abstract String encodeWithCharset(Charset charset) throws UnsupportedEncodingException; + + @Override + public Object exec(List args) throws TemplateModelException { + parent.checkMethodArgCount(args.size(), 1); + try { + String charsetName = (String) args.get(0); + Charset charset = null; + try { + charset = Charset.forName(charsetName); + } catch (UnsupportedCharsetException e) { + throw new _TemplateModelException(e, "Wrong charset name, or charset is unsupported by the runtime " + + "environment: " + _StringUtil.jQuote(charsetName)); + } + return new SimpleScalar(encodeWithCharset(charset)); + } catch (Exception e) { + throw new _TemplateModelException(e, "Failed to execute URL encoding."); + } + } + + @Override + public String getAsString() throws TemplateModelException { + if (cachedResult == null) { + Charset charset = env.getEffectiveURLEscapingCharset(); + if (charset == null) { + throw new _TemplateModelException( + "To do URL encoding, the framework that encloses " + + "FreeMarker must specify the output encoding " + + "or the URL encoding charset, so ask the " + + "programmers to fix it. Or, as a last chance, " + + "you can set the url_encoding_charset setting in " + + "the template, e.g. " + + "<#setting url_escaping_charset='ISO-8859-1'>, or " + + "give the charset explicitly to the buit-in, e.g. " + + "foo?url('ISO-8859-1')."); + } + try { + cachedResult = encodeWithCharset(charset); + } catch (UnsupportedEncodingException e) { + throw new _TemplateModelException(e, "Failed to execute URL encoding."); + } + } + return cachedResult; + } + + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/3fd56062/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java new file mode 100644 index 0000000..21c2a9d --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java @@ -0,0 +1,305 @@ +/* + * 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 java.io.IOException; +import java.io.StringReader; +import java.io.Writer; +import java.util.List; +import java.util.Map; + +import org.apache.freemarker.core.model.ObjectWrapper; +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateMethodModelEx; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.model.TemplateModelException; +import org.apache.freemarker.core.model.TemplateScalarModel; +import org.apache.freemarker.core.model.TemplateSequenceModel; +import org.apache.freemarker.core.model.TemplateTransformModel; +import org.apache.freemarker.core.model.impl.BeanModel; +import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; +import org.apache.freemarker.core.model.impl.SimpleNumber; + +class BuiltInsForStringsMisc { + + // Can't be instantiated + private BuiltInsForStringsMisc() { } + + static class booleanBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + final boolean b; + if (s.equals(MiscUtil.C_TRUE)) { + b = true; + } else if (s.equals(MiscUtil.C_FALSE)) { + b = false; + } else if (s.equals(env.getTemplateBooleanFormat().getTrueStringValue())) { + b = true; + } else if (s.equals(env.getTemplateBooleanFormat().getFalseStringValue())) { + b = false; + } else { + throw new _MiscTemplateException(this, env, + "Can't convert this string to boolean: ", new _DelayedJQuote(s)); + } + return b ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE; + } + } + + static class evalBI extends OutputFormatBoundBuiltIn { + + @Override + protected TemplateModel calculateResult(Environment env) throws TemplateException { + return calculateResult(BuiltInForString.getTargetString(target, env), env); + } + + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + Template parentTemplate = getTemplate(); + + ASTExpression exp = null; + try { + try { + ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration(); + + SimpleCharStream simpleCharStream = new SimpleCharStream( + new StringReader("(" + s + ")"), + RUNTIME_EVAL_LINE_DISPLACEMENT, 1, + s.length() + 2); + simpleCharStream.setTabSize(pCfg.getTabSize()); + FMParserTokenManager tkMan = new FMParserTokenManager( + simpleCharStream); + tkMan.SwitchTo(FMParserConstants.FM_EXPRESSION); + + // pCfg.outputFormat+autoEscapingPolicy is exceptional: it's inherited from the lexical context + FMParser parser = new FMParser( + parentTemplate, false, tkMan, + pCfg, outputFormat, autoEscapingPolicy, + null); + + exp = parser.ASTExpression(); + } catch (TokenMgrError e) { + throw e.toParseException(parentTemplate); + } + } catch (ParseException e) { + throw new _MiscTemplateException(this, env, + "Failed to \"?", key, "\" string with this error:\n\n", + MessageUtil.EMBEDDED_MESSAGE_BEGIN, + new _DelayedGetMessage(e), + MessageUtil.EMBEDDED_MESSAGE_END, + "\n\nThe failing expression:"); + } + try { + return exp.eval(env); + } catch (TemplateException e) { + throw new _MiscTemplateException(this, env, + "Failed to \"?", key, "\" string with this error:\n\n", + MessageUtil.EMBEDDED_MESSAGE_BEGIN, + new _DelayedGetMessageWithoutStackTop(e), + MessageUtil.EMBEDDED_MESSAGE_END, + "\n\nThe failing expression:"); + } + } + + } + + /** + * A method that takes a parameter and evaluates it as a scalar, + * then treats that scalar as template source code and returns a + * transform model that evaluates the template in place. + * The template inherits the configuration and environment of the executing + * template. By default, its name will be equal to + * <tt>executingTemplate.getLookupName() + "$anonymous_interpreted"</tt>. You can + * specify another parameter to the method call in which case the + * template name suffix is the specified id instead of "anonymous_interpreted". + */ + static class interpretBI extends OutputFormatBoundBuiltIn { + + /** + * Constructs a template on-the-fly and returns it embedded in a + * {@link TemplateTransformModel}. + * + * <p>The built-in has two arguments: + * the arguments passed to the method. It can receive at + * least one and at most two arguments, both must evaluate to a scalar. + * The first scalar is interpreted as a template source code and a template + * is built from it. The second (optional) is used to give the generated + * template a name. + * + * @return a {@link TemplateTransformModel} that when executed inside + * a <tt><transform></tt> block will process the generated template + * just as if it had been <tt><transform></tt>-ed at that point. + */ + @Override + protected TemplateModel calculateResult(Environment env) throws TemplateException { + TemplateModel model = target.eval(env); + ASTExpression sourceExpr = null; + String id = "anonymous_interpreted"; + if (model instanceof TemplateSequenceModel) { + sourceExpr = ((ASTExpression) new ASTExpDynamicKeyName(target, new ASTExpNumberLiteral(Integer.valueOf(0))).copyLocationFrom(target)); + if (((TemplateSequenceModel) model).size() > 1) { + id = ((ASTExpression) new ASTExpDynamicKeyName(target, new ASTExpNumberLiteral(Integer.valueOf(1))).copyLocationFrom(target)).evalAndCoerceToPlainText(env); + } + } else if (model instanceof TemplateScalarModel) { + sourceExpr = target; + } else { + throw new UnexpectedTypeException( + target, model, + "sequence or string", new Class[] { TemplateSequenceModel.class, TemplateScalarModel.class }, + env); + } + String templateSource = sourceExpr.evalAndCoerceToPlainText(env); + Template parentTemplate = env.getCurrentTemplate(); + + final Template interpretedTemplate; + try { + ParsingConfiguration pCfg = parentTemplate.getParsingConfiguration(); + // pCfg.outputFormat+autoEscapingPolicy is exceptional: it's inherited from the lexical context + interpretedTemplate = new Template( + (parentTemplate.getLookupName() != null ? parentTemplate.getLookupName() : "nameless_template") + "->" + id, + null, + new StringReader(templateSource), + parentTemplate.getConfiguration(), parentTemplate.getTemplateConfiguration(), + outputFormat, autoEscapingPolicy, + null, null); + } catch (IOException e) { + throw new _MiscTemplateException(this, e, env, + "Template parsing with \"?", key, "\" has failed with this error:\n\n", + MessageUtil.EMBEDDED_MESSAGE_BEGIN, + new _DelayedGetMessage(e), + MessageUtil.EMBEDDED_MESSAGE_END, + "\n\nThe failed expression:"); + } + + return new TemplateProcessorModel(interpretedTemplate); + } + + private class TemplateProcessorModel + implements + TemplateTransformModel { + private final Template template; + + TemplateProcessorModel(Template template) { + this.template = template; + } + + @Override + public Writer getWriter(final Writer out, Map args) throws TemplateModelException, IOException { + try { + Environment env = Environment.getCurrentEnvironment(); + boolean lastFIRE = env.setFastInvalidReferenceExceptions(false); + try { + env.include(template); + } finally { + env.setFastInvalidReferenceExceptions(lastFIRE); + } + } catch (Exception e) { + throw new _TemplateModelException(e, + "Template created with \"?", key, "\" has stopped with this error:\n\n", + MessageUtil.EMBEDDED_MESSAGE_BEGIN, + new _DelayedGetMessage(e), + MessageUtil.EMBEDDED_MESSAGE_END); + } + + return new Writer(out) + { + @Override + public void close() { + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + out.write(cbuf, off, len); + } + }; + } + } + + } + + static class numberBI extends BuiltInForString { + @Override + TemplateModel calculateResult(String s, Environment env) throws TemplateException { + try { + return new SimpleNumber(env.getArithmeticEngine().toNumber(s)); + } catch (NumberFormatException nfe) { + throw NonNumericalException.newMalformedNumberException(this, s, env); + } + } + } + + /** + * A built-in that allows us to instantiate an instance of a java class. + * Usage is something like: <tt><#assign foobar = "foo.bar.MyClass"?new()></tt>; + */ + static class newBI extends ASTExpBuiltIn { + + @Override + TemplateModel _eval(Environment env) + throws TemplateException { + return new ConstructorFunction(target.evalAndCoerceToPlainText(env), env, target.getTemplate()); + } + + class ConstructorFunction implements TemplateMethodModelEx { + + private final Class<?> cl; + private final Environment env; + + public ConstructorFunction(String classname, Environment env, Template template) throws TemplateException { + this.env = env; + cl = env.getNewBuiltinClassResolver().resolve(classname, env, template); + if (!TemplateModel.class.isAssignableFrom(cl)) { + throw new _MiscTemplateException(newBI.this, env, + "Class ", cl.getName(), " does not implement org.apache.freemarker.core.TemplateModel"); + } + if (BeanModel.class.isAssignableFrom(cl)) { + throw new _MiscTemplateException(newBI.this, env, + "Bean Models cannot be instantiated using the ?", key, " built-in"); + } + } + + @Override + public Object exec(List arguments) throws TemplateModelException { + ObjectWrapper ow = env.getObjectWrapper(); + if (ow instanceof DefaultObjectWrapper) { + return ((DefaultObjectWrapper) ow).newInstance(cl, arguments); + } + + if (!arguments.isEmpty()) { + throw new TemplateModelException( + "className?new(args) only supports 0 arguments in the current configuration, because " + + " the objectWrapper setting value is not a " + + DefaultObjectWrapper.class.getName() + + " (or its subclass)."); + } + try { + return cl.newInstance(); + } catch (Exception e) { + throw new TemplateModelException("Failed to instantiate " + + cl.getName() + " with its parameterless constructor; see cause exception", e); + } + } + } + } + +}
