in freemarker-core/src/main/java/freemarker/core/DynamicKeyName.java [183:338]
private TemplateModel dealWithRangeKey(TemplateModel targetModel, RangeModel range, Environment env)
throws TemplateException {
// We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string
final TemplateSequenceModel targetSeq;
final LazilyGeneratedCollectionModel targetLazySeq;
final String targetStr;
if (targetModel instanceof TemplateSequenceModel) {
targetSeq = (TemplateSequenceModel) targetModel;
targetLazySeq = null;
targetStr = null;
} else if (targetModel instanceof LazilyGeneratedCollectionModel
&& ((LazilyGeneratedCollectionModel) targetModel).isSequence()) {
targetSeq = null;
targetLazySeq = (LazilyGeneratedCollectionModel) targetModel;
targetStr = null;
} else {
targetSeq = null;
targetLazySeq = 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 rangeSize = range.size(); // Warning: Value is meaningless for right unbounded sequences
final boolean rightUnbounded = range.isRightUnbounded();
final boolean rightAdaptive = range.isRightAdaptive(); // Always true if rightUnbounded
// 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 && rangeSize == 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 step = range.getStep(); // Currently always 1 or -1
final int targetSize;
final boolean targetSizeKnown; // Didn't want to use targetSize = -1, as we don't control the seq.size() impl.
if (targetStr != null) {
targetSize = targetStr.length();
targetSizeKnown = true;
} else if (targetSeq != null) {
targetSize = targetSeq.size();
targetSizeKnown = true;
} else if (targetLazySeq instanceof TemplateCollectionModelEx) {
// E.g. the size of seq?map(f) is known, despite that the elements are lazily calculated.
targetSize = ((TemplateCollectionModelEx) targetLazySeq).size();
targetSizeKnown = true;
} else {
targetSize = Integer.MAX_VALUE;
targetSizeKnown = false;
}
if (targetSizeKnown) {
// 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. Hence 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 too high firstIndex.
// Right-bounded ranges at this point aren't empty, so firstIndex == targetSize can't be allowed.
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).");
}
}
// Calculate resultSize. Note that:
// - It might will be UNKNOWN_RESULT_SIZE when targetLazySeq != null.
// - It might will be out-of-bounds if !targetSizeKnown (otherwise we validate if the range is correct)
final int resultSize;
if (!rightUnbounded) {
final int lastIdx = firstIdx + (rangeSize - 1) * step; // Note: lastIdx is inclusive
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 (targetSizeKnown && 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 = rangeSize;
}
} else { // rightUnbounded
resultSize = targetSizeKnown ? targetSize - firstIdx : UNKNOWN_RESULT_SIZE;
}
if (resultSize == 0) {
return emptyResult(targetSeq != null);
}
if (targetSeq != null) {
// In principle we should take lazilyGeneratedResultEnabled into account, but that wouldn't be backward
// compatible. For example, with lazily generated sequence result <#list xs[b..e] as x> would behave
// differently if xs is modified inside the #list nested content.
ArrayList<TemplateModel> resultList = new ArrayList<>(resultSize);
int srcIdx = firstIdx;
for (int i = 0; i < resultSize; i++) {
resultList.add(targetSeq.get(srcIdx));
srcIdx += step;
}
// List items are already wrapped:
return new SimpleSequence(resultList, _ObjectWrappers.SAFE_OBJECT_WRAPPER);
} else if (targetLazySeq != null) {
// As a targetLazySeq can only occur if a new built-in like ?filter or ?map was used somewhere in the target
// expression, in this case we can return lazily generated sequence without breaking backward compatibility.
if (step == 1) {
return getStep1RangeFromIterator(targetLazySeq.iterator(), range, resultSize, targetSizeKnown);
} else if (step == -1) {
return getStepMinus1RangeFromIterator(targetLazySeq.iterator(), range, resultSize);
} else {
throw new AssertionError();
}
} else {
final int exclEndIdx;
if (step < 0 && resultSize > 1) {
if (!(range.isAffectedByStringSlicingBug() && 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));
}
}