Return-Path: X-Original-To: archive-asf-public-internal@cust-asf2.ponee.io Delivered-To: archive-asf-public-internal@cust-asf2.ponee.io Received: from cust-asf.ponee.io (cust-asf.ponee.io [163.172.22.183]) by cust-asf2.ponee.io (Postfix) with ESMTP id 076A1200C24 for ; Thu, 23 Feb 2017 22:37:16 +0100 (CET) Received: by cust-asf.ponee.io (Postfix) id 05EB8160B3E; Thu, 23 Feb 2017 21:37:16 +0000 (UTC) Delivered-To: archive-asf-public@cust-asf.ponee.io Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by cust-asf.ponee.io (Postfix) with SMTP id 9B3F7160B78 for ; Thu, 23 Feb 2017 22:37:13 +0100 (CET) Received: (qmail 96534 invoked by uid 500); 23 Feb 2017 21:37:12 -0000 Mailing-List: contact notifications-help@freemarker.incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@freemarker.incubator.apache.org Delivered-To: mailing list notifications@freemarker.incubator.apache.org Received: (qmail 96520 invoked by uid 99); 23 Feb 2017 21:37:12 -0000 Received: from pnap-us-west-generic-nat.apache.org (HELO spamd4-us-west.apache.org) (209.188.14.142) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 23 Feb 2017 21:37:12 +0000 Received: from localhost (localhost [127.0.0.1]) by spamd4-us-west.apache.org (ASF Mail Server at spamd4-us-west.apache.org) with ESMTP id 37147C20ED for ; Thu, 23 Feb 2017 21:37:12 +0000 (UTC) X-Virus-Scanned: Debian amavisd-new at spamd4-us-west.apache.org X-Spam-Flag: NO X-Spam-Score: -6.219 X-Spam-Level: X-Spam-Status: No, score=-6.219 tagged_above=-999 required=6.31 tests=[KAM_ASCII_DIVIDERS=0.8, KAM_LAZY_DOMAIN_SECURITY=1, RCVD_IN_DNSWL_HI=-5, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01, RP_MATCHES_RCVD=-2.999] autolearn=disabled Received: from mx1-lw-us.apache.org ([10.40.0.8]) by localhost (spamd4-us-west.apache.org [10.40.0.11]) (amavisd-new, port 10024) with ESMTP id QpJoEZuFdewL for ; Thu, 23 Feb 2017 21:37:01 +0000 (UTC) Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by mx1-lw-us.apache.org (ASF Mail Server at mx1-lw-us.apache.org) with SMTP id 191AE61F0E for ; Thu, 23 Feb 2017 21:36:46 +0000 (UTC) Received: (qmail 85798 invoked by uid 99); 23 Feb 2017 21:35:29 -0000 Received: from git1-us-west.apache.org (HELO git1-us-west.apache.org) (140.211.11.23) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 23 Feb 2017 21:35:29 +0000 Received: by git1-us-west.apache.org (ASF Mail Server at git1-us-west.apache.org, from userid 33) id AB829DFDAC; Thu, 23 Feb 2017 21:35:29 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: ddekany@apache.org To: notifications@freemarker.incubator.apache.org Date: Thu, 23 Feb 2017 21:36:11 -0000 Message-Id: In-Reply-To: References: X-Mailer: ASF-Git Admin Mailer Subject: [44/54] [partial] incubator-freemarker git commit: Unifying the o.a.f.core and o.a.f.core.ast archived-at: Thu, 23 Feb 2017 21:37:16 -0000 http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java b/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java new file mode 100644 index 0000000..142d924 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java @@ -0,0 +1,878 @@ +/* + * 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.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.model.impl.beans.CollectionModel; +import org.apache.freemarker.core.model.Constants; +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 && !isBuggySeqButGoodCollection(model)) { + 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 && !isBuggySeqButGoodCollection(model)) { + 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 + && !isBuggySeqButGoodCollection(model) + ? (TemplateSequenceModel) model + : null; + // 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 sort and 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 null. + * @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 not found."); + } + } // 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 isBuggySeqButGoodCollection( + TemplateModel model) { + return model instanceof CollectionModel + ? !((CollectionModel) model).getSupportsIndexedAccess() + : false; + } + + 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/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java new file mode 100644 index 0000000..0bb09bf --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsBasic.java @@ -0,0 +1,698 @@ +/* + * 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.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.model.impl.SimpleSequence; +import org.apache.freemarker.core.model.impl._StaticObjectWrappers; +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 _StaticObjectWrappers.DEFAULT_OBJECT_WRAPPER.wrap(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) { + SimpleSequence result = new SimpleSequence(); + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) { + result.add(st.nextToken()); + } + return result; + } + } + + // Can't be instantiated + private BuiltInsForStringsBasic() { } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java new file mode 100644 index 0000000..28dd510 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsEncoding.java @@ -0,0 +1,185 @@ +/* + * 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.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(String cs) throws UnsupportedEncodingException { + return _StringUtil.URLEnc(targetAsString, cs); + } + + } + + @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(String cs) throws UnsupportedEncodingException { + return _StringUtil.URLPathEnc(targetAsString, cs); + } + + } + + @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(String cs) throws UnsupportedEncodingException; + + @Override + public Object exec(List args) throws TemplateModelException { + parent.checkMethodArgCount(args.size(), 1); + try { + return new SimpleScalar(encodeWithCharset((String) args.get(0))); + } catch (UnsupportedEncodingException e) { + throw new _TemplateModelException(e, "Failed to execute URL encoding."); + } + } + + @Override + public String getAsString() throws TemplateModelException { + if (cachedResult == null) { + String cs = env.getEffectiveURLEscapingCharset(); + if (cs == 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(cs); + } 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/7d784b2b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java new file mode 100644 index 0000000..543143a --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/BuiltInsForStringsMisc.java @@ -0,0 +1,301 @@ +/* + * 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.SimpleNumber; +import org.apache.freemarker.core.model.impl._StaticObjectWrappers; +import org.apache.freemarker.core.model.impl.beans.BeanModel; +import org.apache.freemarker.core.model.impl.beans.BeansWrapper; + +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("true")) { + b = true; + } else if (s.equals("false")) { + b = false; + } else if (s.equals(env.getTrueStringValue())) { + b = true; + } else if (s.equals(env.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 { + ParserConfiguration pCfg = parentTemplate.getParserConfiguration(); + + 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 is exceptional: it's inherited from the lexical context + if (pCfg.getOutputFormat() != outputFormat) { + pCfg = new _ParserConfigurationWithInheritedFormat( + pCfg, outputFormat, Integer.valueOf(autoEscapingPolicy)); + } + + FMParser parser = new FMParser( + parentTemplate, false, tkMan, pCfg, TemplateSpecifiedEncodingHandler.DEFAULT); + + 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 + * executingTemplate.getName() + "$anonymous_interpreted". 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}. + * + *

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 <transform> block will process the generated template + * just as if it had been <transform>-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 { + ParserConfiguration pCfg = parentTemplate.getParserConfiguration(); + // pCfg.outputFormat is exceptional: it's inherited from the lexical context + if (pCfg.getOutputFormat() != outputFormat) { + pCfg = new _ParserConfigurationWithInheritedFormat( + pCfg, outputFormat, Integer.valueOf(autoEscapingPolicy)); + } + interpretedTemplate = new Template( + (parentTemplate.getName() != null ? parentTemplate.getName() : "nameless_template") + "->" + id, + null, + new StringReader(templateSource), + parentTemplate.getConfiguration(), pCfg, + null); + } catch (IOException e) { + throw new _MiscTemplateException(this, e, env, new Object[] { + "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:" }); + } + + interpretedTemplate.setLocale(env.getLocale()); + 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: <#assign foobar = "foo.bar.MyClass"?new()>; + */ + 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(); + BeansWrapper bw = + ow instanceof BeansWrapper + ? (BeansWrapper) ow + : _StaticObjectWrappers.BEANS_WRAPPER; + return bw.newInstance(cl, arguments); + } + } + } + +}