/*
 * 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 freemarker.core;

import java.util.ArrayList;
import java.util.Collections;

import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template._TemplateAPI;
import freemarker.template.utility.Constants;

target[keyExpression], where, in FM 2.3, keyExpression can be string, a number or a range, and target can be a hash or a sequence.
/** * {@code target[keyExpression]}, where, in FM 2.3, {@code keyExpression} can be string, a number or a range, * and {@code target} can be a hash or a sequence. */
final class DynamicKeyName extends Expression { private final Expression keyExpression; private final Expression target; DynamicKeyName(Expression target, Expression keyExpression) { this.target = target; this.keyExpression = keyExpression; } @Override TemplateModel _eval(Environment env) throws TemplateException { TemplateModel targetModel = target.eval(env); if (targetModel == null) { if (env.isClassicCompatible()) { return null; } else { throw InvalidReferenceException.getInstance(target, env); } } TemplateModel keyModel = keyExpression.eval(env); if (keyModel == null) { if (env.isClassicCompatible()) { keyModel = TemplateScalarModel.EMPTY_STRING; } else { keyExpression.assertNonNull(null, env); } } if (keyModel instanceof TemplateNumberModel) { int index = keyExpression.modelToNumber(keyModel, env).intValue(); return dealWithNumericalKey(targetModel, index, env); } if (keyModel instanceof TemplateScalarModel) { String key = EvalUtil.modelToString((TemplateScalarModel) keyModel, keyExpression, env); return dealWithStringKey(targetModel, key, env); } if (keyModel instanceof RangeModel) { return dealWithRangeKey(targetModel, (RangeModel) keyModel, env); } throw new UnexpectedTypeException(keyExpression, keyModel, "number, range, or string", new Class[] { TemplateNumberModel.class, TemplateScalarModel.class, Range.class }, env); } static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES; static { NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + NonStringException.STRING_COERCABLE_TYPES.length]; NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class; for (int i = 0; i < NonStringException.STRING_COERCABLE_TYPES.length; i++) { NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = NonStringException.STRING_COERCABLE_TYPES[i]; } } private TemplateModel dealWithNumericalKey(TemplateModel targetModel, int index, Environment env) throws TemplateException { if (targetModel instanceof TemplateSequenceModel) { TemplateSequenceModel tsm = (TemplateSequenceModel) targetModel; int size; try { size = tsm.size(); } catch (Exception e) { size = Integer.MAX_VALUE; } return index < size ? tsm.get(index) : null; } try { String s = target.evalAndCoerceToPlainText(env); try { return new SimpleScalar(s.substring(index, index + 1)); } catch (IndexOutOfBoundsException e) { if (index < 0) { throw new _MiscTemplateException("Negative index not allowed: ", Integer.valueOf(index)); } if (index >= s.length()) { throw new _MiscTemplateException( "String index out of range: The index was ", Integer.valueOf(index), " (0-based), but the length of the string is only ", Integer.valueOf(s.length()) , "."); } throw new RuntimeException("Can't explain exception", e); } } catch (NonStringException e) { throw new UnexpectedTypeException( target, targetModel, "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC, NUMERICAL_KEY_LHO_EXPECTED_TYPES, (targetModel instanceof TemplateHashModel ? "You had a numberical value inside the []. Currently that's only supported for " + "sequences (lists) and strings. To get a Map item with a non-string key, " + "use myMap?api.get(myKey)." : null), env); } } private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env) throws TemplateException { if (targetModel instanceof TemplateHashModel) { return((TemplateHashModel) targetModel).get(key); } throw new NonHashException(target, targetModel, env); } private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env) throws UnexpectedTypeException, InvalidReferenceException, TemplateException { final TemplateSequenceModel targetSeq; final String targetStr; if (targetModel instanceof TemplateSequenceModel) { targetSeq = (TemplateSequenceModel) targetModel; targetStr = null; } else { targetSeq = null; try { targetStr = target.evalAndCoerceToPlainText(env); } catch (NonStringException e) { throw new UnexpectedTypeException( target, target.eval(env), "sequence or " + NonStringException.STRING_COERCABLE_TYPES_DESC, NUMERICAL_KEY_LHO_EXPECTED_TYPES, env); } } final int size = range.size(); final boolean rightUnbounded = range.isRightUnbounded(); final boolean rightAdaptive = range.isRightAdaptive(); // Right bounded empty ranges are accepted even if the begin index is out of bounds. That's because a such range // produces an empty sequence, which thus doesn't contain any illegal indexes. if (!rightUnbounded && size == 0) { return emptyResult(targetSeq != null); } final int firstIdx = range.getBegining(); if (firstIdx < 0) { throw new _MiscTemplateException(keyExpression, "Negative range start index (", Integer.valueOf(firstIdx), ") isn't allowed for a range used for slicing."); } final int targetSize = targetStr != null ? targetStr.length() : targetSeq.size(); final int step = range.getStep(); // Right-adaptive increasing ranges can start 1 after the last element of the target, because they are like // ranges with exclusive end index of at most targetSize. Thence a such range is just an empty list of indexes, // and thus it isn't out-of-bounds. // Right-adaptive decreasing ranges has exclusive end -1, so it can't help on a to high firstIndex. // Right-bounded ranges at this point aren't empty, so the right index surely can't reach targetSize. if (rightAdaptive && step == 1 ? firstIdx > targetSize : firstIdx >= targetSize) { throw new _MiscTemplateException(keyExpression, "Range start index ", Integer.valueOf(firstIdx), " is out of bounds, because the sliced ", (targetStr != null ? "string" : "sequence"), " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"), ". ", "(Note that indices are 0-based)."); } final int resultSize; if (!rightUnbounded) { final int lastIdx = firstIdx + (size - 1) * step; if (lastIdx < 0) { if (!rightAdaptive) { throw new _MiscTemplateException(keyExpression, "Negative range end index (", Integer.valueOf(lastIdx), ") isn't allowed for a range used for slicing."); } else { resultSize = firstIdx + 1; } } else if (lastIdx >= targetSize) { if (!rightAdaptive) { throw new _MiscTemplateException(keyExpression, "Range end index ", Integer.valueOf(lastIdx), " is out of bounds, because the sliced ", (targetStr != null ? "string" : "sequence"), " has only ", Integer.valueOf(targetSize), " ", (targetStr != null ? "character(s)" : "element(s)"), ". (Note that indices are 0-based)."); } else { resultSize = Math.abs(targetSize - firstIdx); } } else { resultSize = size; } } else { resultSize = targetSize - firstIdx; } if (resultSize == 0) { return emptyResult(targetSeq != null); } if (targetSeq != null) { ArrayList/*<TemplateModel>*/ list = new ArrayList(resultSize); int srcIdx = firstIdx; for (int i = 0; i < resultSize; i++) { list.add(targetSeq.get(srcIdx)); srcIdx += step; } // List items are already wrapped, so the wrapper will be null: return new SimpleSequence(list, null); } else { final int exclEndIdx; if (step < 0 && resultSize > 1) { if (!(range.isAffactedByStringSlicingBug() && resultSize == 2)) { throw new _MiscTemplateException(keyExpression, "Decreasing ranges aren't allowed for slicing strings (as it would give reversed text). " + "The index range was: first = ", Integer.valueOf(firstIdx), ", last = ", Integer.valueOf(firstIdx + (resultSize - 1) * step)); } else { // Emulate the legacy bug, where "foo"[n .. n-1] gives "" instead of an error (if n >= 1). // Fix this in FTL [2.4] exclEndIdx = firstIdx; } } else { exclEndIdx = firstIdx + resultSize; } return new SimpleScalar(targetStr.substring(firstIdx, exclEndIdx)); } } private TemplateModel emptyResult(boolean seq) { return seq ? (_TemplateAPI.getTemplateLanguageVersionAsInt(this) < _TemplateAPI.VERSION_INT_2_3_21 ? new SimpleSequence(Collections.EMPTY_LIST, null) : Constants.EMPTY_SEQUENCE) : TemplateScalarModel.EMPTY_STRING; } @Override public String getCanonicalForm() { return target.getCanonicalForm() + "[" + keyExpression.getCanonicalForm() + "]"; } @Override String getNodeTypeSymbol() { return "...[...]"; } @Override boolean isLiteral() { return constantValue != null || (target.isLiteral() && keyExpression.isLiteral()); } @Override int getParameterCount() { return 2; } @Override Object getParameterValue(int idx) { return idx == 0 ? target : keyExpression; } @Override ParameterRole getParameterRole(int idx) { return idx == 0 ? ParameterRole.LEFT_HAND_OPERAND : ParameterRole.ENCLOSED_OPERAND; } @Override protected Expression deepCloneWithIdentifierReplaced_inner( String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) { return new DynamicKeyName( target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState), keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState)); } }