blob: 86eb02d6ec1c70455af6d909ef856672b062e2c1 [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 freemarker.core;
import java.util.HashSet;
import java.util.Set;
import freemarker.template.SimpleNumber;
import freemarker.template.SimpleScalar;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;
import freemarker.template._TemplateAPI;
/**
* An operator for the + operator. Note that this is treated
* separately from the other 4 arithmetic operators,
* since + is overloaded to mean string concatenation.
*/
final class AddConcatExpression extends Expression {
private final Expression left;
private final Expression right;
AddConcatExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
TemplateModel _eval(Environment env) throws TemplateException {
return _eval(env, this, left, left.eval(env), right, right.eval(env));
}
/**
* @param leftExp
* Used for error messages only; can be {@code null}
* @param rightExp
* Used for error messages only; can be {@code null}
*/
static TemplateModel _eval(Environment env,
TemplateObject parent,
Expression leftExp, TemplateModel leftModel,
Expression rightExp, TemplateModel rightModel)
throws TemplateModelException, TemplateException, NonStringException {
if (leftModel instanceof TemplateNumberModel && rightModel instanceof TemplateNumberModel) {
Number first = EvalUtil.modelToNumber((TemplateNumberModel) leftModel, leftExp);
Number second = EvalUtil.modelToNumber((TemplateNumberModel) rightModel, rightExp);
return _evalOnNumbers(env, parent, first, second);
} else if (leftModel instanceof TemplateSequenceModel && rightModel instanceof TemplateSequenceModel) {
return new ConcatenatedSequence((TemplateSequenceModel) leftModel, (TemplateSequenceModel) rightModel);
} else {
boolean hashConcatPossible
= leftModel instanceof TemplateHashModel && rightModel instanceof TemplateHashModel;
try {
// We try string addition first. If hash addition is possible, then instead of throwing exception
// we return null and do hash addition instead. (We can't simply give hash addition a priority, like
// with sequence addition above, as FTL strings are often also FTL hashes.)
Object leftOMOrStr = EvalUtil.coerceModelToStringOrMarkup(
leftModel, leftExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
env);
if (leftOMOrStr == null) {
return _eval_concatenateHashes(leftModel, rightModel);
}
// Same trick with null return as above.
Object rightOMOrStr = EvalUtil.coerceModelToStringOrMarkup(
rightModel, rightExp, /* returnNullOnNonCoercableType = */ hashConcatPossible, null,
env);
if (rightOMOrStr == null) {
return _eval_concatenateHashes(leftModel, rightModel);
}
if (leftOMOrStr instanceof String) {
if (rightOMOrStr instanceof String) {
return new SimpleScalar(((String) leftOMOrStr).concat((String) rightOMOrStr));
} else { // rightOMOrStr instanceof TemplateMarkupOutputModel
TemplateMarkupOutputModel<?> rightMO = (TemplateMarkupOutputModel<?>) rightOMOrStr;
return EvalUtil.concatMarkupOutputs(parent,
rightMO.getOutputFormat().fromPlainTextByEscaping((String) leftOMOrStr),
rightMO);
}
} else { // leftOMOrStr instanceof TemplateMarkupOutputModel
TemplateMarkupOutputModel<?> leftMO = (TemplateMarkupOutputModel<?>) leftOMOrStr;
if (rightOMOrStr instanceof String) { // markup output
return EvalUtil.concatMarkupOutputs(parent,
leftMO,
leftMO.getOutputFormat().fromPlainTextByEscaping((String) rightOMOrStr));
} else { // rightOMOrStr instanceof TemplateMarkupOutputModel
return EvalUtil.concatMarkupOutputs(parent,
leftMO,
(TemplateMarkupOutputModel<?>) rightOMOrStr);
}
}
} catch (NonStringOrTemplateOutputException e) {
// 2.4: Remove this catch; it's for BC, after reworking hash addition so it doesn't rely on this. But
// user code might throws this (very unlikely), and then in 2.3.x we did catch that too, incorrectly.
if (hashConcatPossible) {
return _eval_concatenateHashes(leftModel, rightModel);
} else {
throw e;
}
}
}
}
private static TemplateModel _eval_concatenateHashes(TemplateModel leftModel, TemplateModel rightModel)
throws TemplateModelException {
if (leftModel instanceof TemplateHashModelEx && rightModel instanceof TemplateHashModelEx) {
TemplateHashModelEx leftModelEx = (TemplateHashModelEx) leftModel;
TemplateHashModelEx rightModelEx = (TemplateHashModelEx) rightModel;
if (leftModelEx.size() == 0) {
return rightModelEx;
} else if (rightModelEx.size() == 0) {
return leftModelEx;
} else {
return new ConcatenatedHashEx(leftModelEx, rightModelEx);
}
} else {
return new ConcatenatedHash((TemplateHashModel) leftModel,
(TemplateHashModel) rightModel);
}
}
static TemplateModel _evalOnNumbers(Environment env, TemplateObject parent, Number first, Number second)
throws TemplateException {
ArithmeticEngine ae = EvalUtil.getArithmeticEngine(env, parent);
return new SimpleNumber(ae.add(first, second));
}
@Override
boolean isLiteral() {
return constantValue != null || (left.isLiteral() && right.isLiteral());
}
@Override
protected Expression deepCloneWithIdentifierReplaced_inner(
String replacedIdentifier, Expression replacement, ReplacemenetState replacementState) {
return new AddConcatExpression(
left.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState),
right.deepCloneWithIdentifierReplaced(replacedIdentifier, replacement, replacementState));
}
@Override
public String getCanonicalForm() {
return left.getCanonicalForm() + " + " + right.getCanonicalForm();
}
@Override
String getNodeTypeSymbol() {
return "+";
}
@Override
int getParameterCount() {
return 2;
}
@Override
Object getParameterValue(int idx) {
return idx == 0 ? left : right;
}
@Override
ParameterRole getParameterRole(int idx) {
return ParameterRole.forBinaryOperatorOperand(idx);
}
private static final class ConcatenatedSequence
implements
TemplateSequenceModel {
private final TemplateSequenceModel left;
private final TemplateSequenceModel right;
ConcatenatedSequence(TemplateSequenceModel left, TemplateSequenceModel right) {
this.left = left;
this.right = right;
}
public int size()
throws TemplateModelException {
return left.size() + right.size();
}
public TemplateModel get(int i)
throws TemplateModelException {
int ls = left.size();
return i < ls ? left.get(i) : right.get(i - ls);
}
}
private static class ConcatenatedHash
implements TemplateHashModel {
protected final TemplateHashModel left;
protected final TemplateHashModel right;
ConcatenatedHash(TemplateHashModel left, TemplateHashModel right) {
this.left = left;
this.right = right;
}
public TemplateModel get(String key)
throws TemplateModelException {
TemplateModel model = right.get(key);
return (model != null) ? model : left.get(key);
}
public boolean isEmpty()
throws TemplateModelException {
return left.isEmpty() && right.isEmpty();
}
}
private static final class ConcatenatedHashEx
extends ConcatenatedHash
implements TemplateHashModelEx {
private CollectionAndSequence keys;
private CollectionAndSequence values;
ConcatenatedHashEx(TemplateHashModelEx left, TemplateHashModelEx right) {
super(left, right);
}
public int size() throws TemplateModelException {
initKeys();
return keys.size();
}
public TemplateCollectionModel keys()
throws TemplateModelException {
initKeys();
return keys;
}
public TemplateCollectionModel values()
throws TemplateModelException {
initValues();
return values;
}
private void initKeys()
throws TemplateModelException {
if (keys == null) {
HashSet keySet = new HashSet();
SimpleSequence keySeq = new SimpleSequence(32, _TemplateAPI.SAFE_OBJECT_WRAPPER);
addKeys(keySet, keySeq, (TemplateHashModelEx) this.left);
addKeys(keySet, keySeq, (TemplateHashModelEx) this.right);
keys = new CollectionAndSequence(keySeq);
}
}
private static void addKeys(Set keySet, SimpleSequence keySeq, TemplateHashModelEx hash)
throws TemplateModelException {
TemplateModelIterator it = hash.keys().iterator();
while (it.hasNext()) {
TemplateScalarModel tsm = (TemplateScalarModel) it.next();
if (keySet.add(tsm.getAsString())) {
// The first occurrence of the key decides the index;
// this is consistent with the behavior of java.util.LinkedHashSet.
keySeq.add(tsm);
}
}
}
private void initValues()
throws TemplateModelException {
if (values == null) {
SimpleSequence seq = new SimpleSequence(size(), _TemplateAPI.SAFE_OBJECT_WRAPPER);
// Note: size() invokes initKeys() if needed.
int ln = keys.size();
for (int i = 0; i < ln; i++) {
seq.add(get(((TemplateScalarModel) keys.get(i)).getAsString()));
}
values = new CollectionAndSequence(seq);
}
}
}
}