| /* |
| * 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; |
| |
| /** |
| * {@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)); |
| } |
| } |