private TemplateModel dealWithRangeKey()

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));
        }
    }