blob: 9e6087d9d1cfa948cb879b4e3f3b83a396ab6bfa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.freemarker.core;
import org.apache.freemarker.core.model.TemplateHashModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateNumberModel;
import org.apache.freemarker.core.model.TemplateSequenceModel;
import org.apache.freemarker.core.model.TemplateStringModel;
import org.apache.freemarker.core.model.impl.SimpleString;
/**
* AST expression node: {@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 ASTExpDynamicKeyName extends ASTExpression {
private final ASTExpression keyExpression;
private final ASTExpression target;
ASTExpDynamicKeyName(ASTExpression target, ASTExpression keyExpression) {
this.target = target;
this.keyExpression = keyExpression;
}
@Override
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel targetModel = target.eval(env);
target.assertNonNull(targetModel, env);
TemplateModel keyModel = keyExpression.eval(env);
keyExpression.assertNonNull(keyModel, env);
if (keyModel instanceof TemplateNumberModel) {
int index = keyExpression.modelToNumber(keyModel, env).intValue();
return dealWithNumericalKey(targetModel, index, env);
}
if (keyModel instanceof TemplateStringModel) {
String key = _EvalUtils.modelToString((TemplateStringModel) keyModel, keyExpression);
return dealWithStringKey(targetModel, key, env);
}
if (keyModel instanceof RangeModel) {
return dealWithRangeKey(targetModel, (RangeModel) keyModel, env);
}
throw MessageUtils.newUnexpectedOperandTypeException(keyExpression, keyModel,
"number, range, or string",
new Class[] { TemplateNumberModel.class, TemplateStringModel.class, ASTExpRange.class },
null, env);
}
static private Class[] NUMERICAL_KEY_LHO_EXPECTED_TYPES;
static {
NUMERICAL_KEY_LHO_EXPECTED_TYPES = new Class[1 + MessageUtils.EXPECTED_TYPES_STRING_COERCABLE.length];
NUMERICAL_KEY_LHO_EXPECTED_TYPES[0] = TemplateSequenceModel.class;
for (int i = 0; i < MessageUtils.EXPECTED_TYPES_STRING_COERCABLE.length; i++) {
NUMERICAL_KEY_LHO_EXPECTED_TYPES[i + 1] = MessageUtils.EXPECTED_TYPES_STRING_COERCABLE[i];
}
}
private TemplateModel dealWithNumericalKey(TemplateModel targetModel,
int index,
Environment env)
throws TemplateException {
if (targetModel instanceof TemplateSequenceModel) {
return ((TemplateSequenceModel) targetModel).get(index);
}
String s;
try {
s = target.evalAndCoerceToPlainText(env);
} catch (TemplateException e) {
// TODO [FM3] Wrong, as we don't know why this was thrown. I think we just shouldn't coerce.
throw MessageUtils.newUnexpectedOperandTypeException(
target, targetModel,
"sequence or " + MessageUtils.STRING_COERCABLE_TYPES_DESC,
NUMERICAL_KEY_LHO_EXPECTED_TYPES,
(targetModel instanceof TemplateHashModel
? new Object[] { "You had a numerical 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);
}
try {
return new SimpleString(s.substring(index, index + 1));
} catch (IndexOutOfBoundsException e) {
if (index < 0) {
throw new TemplateException("Negative index not allowed: ", Integer.valueOf(index));
}
if (index >= s.length()) {
throw new TemplateException(
"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);
}
}
private TemplateModel dealWithStringKey(TemplateModel targetModel, String key, Environment env)
throws TemplateException {
if (targetModel instanceof TemplateHashModel) {
return((TemplateHashModel) targetModel).get(key);
}
throw MessageUtils.newUnexpectedOperandTypeException(target, targetModel, TemplateHashModel.class, env);
}
private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
throws 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 (TemplateException e) {
// TODO [FM3] Wrong, as we don't know why this was thrown. I think we just shouldn't coerce.
throw MessageUtils.newUnexpectedOperandTypeException(
target, target.eval(env),
"sequence or " + MessageUtils.STRING_COERCABLE_TYPES_DESC,
NUMERICAL_KEY_LHO_EXPECTED_TYPES, null, env);
}
}
final int size = range.getCollectionSize();
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.getBeginning();
if (firstIdx < 0) {
throw new TemplateException(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.getCollectionSize();
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 TemplateException(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 TemplateException(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 TemplateException(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) {
NativeSequence resultSeq = new NativeSequence(resultSize);
int srcIdx = firstIdx;
for (int i = 0; i < resultSize; i++) {
resultSeq.add(targetSeq.get(srcIdx));
srcIdx += step;
}
// List items are already wrapped, so the wrapper will be null:
return resultSeq;
} else {
if (step < 0 && resultSize > 1) {
throw new TemplateException(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));
}
return new SimpleString(targetStr.substring(firstIdx, firstIdx + resultSize));
}
}
private TemplateModel emptyResult(boolean seq) {
return seq ? TemplateSequenceModel.EMPTY_SEQUENCE : TemplateStringModel.EMPTY_STRING;
}
@Override
public String getCanonicalForm() {
return target.getCanonicalForm()
+ "["
+ keyExpression.getCanonicalForm()
+ "]";
}
@Override
public String getLabelWithoutParameters() {
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
ASTExpression deepCloneWithIdentifierReplaced_inner(
String replacedIdentifier, ASTExpression replacement, ReplacemenetState replacementState) {
return new ASTExpDynamicKeyName(
target.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
keyExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
}
}