ExpressionLanguageJEXLImpl.java

package com.github.mygreen.supercsv.expression;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.jexl3.introspection.JexlPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.mygreen.supercsv.util.Utils;


/**
 * 式言語<a href="http://commons.apache.org/proper/commons-jexl/" target="_blank">JEXL(Java Expression Language)</a>の実装。
 * <p>利用する際には、JEXL v3.3以上のライブラリが必要です。
 * <p>JEXL v3.3から、ELインジェクション対策として、<a href="https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/introspection/JexlPermissions.html)">JexlPermissions</a>によるEL式中で参照/実行可能なクラスを制限されます。
 * <p>独自のCellProcessorなどを実装しているが場合は、システムプロパティ {@literal supercsv.annotation.jexlPermissions} で指定することができます。
 *    複数指定する場合はカンマ区切りで指定します。
 * </p>
 *
 * @version 2.4
 * @since 2.0
 * @author T.TSUCHIE
 *
 */
public class ExpressionLanguageJEXLImpl implements ExpressionLanguage {
    
    private static final Logger logger = LoggerFactory.getLogger(ExpressionLanguageJEXLImpl.class);
    
    /**
     * システムプロパティ - JEXLをRESTRICTモードで使用するかどうかフラグ。
     */
    public static final String PROPERTY_JEXL_RESTRICTED = "supercsv.annotation.jexlRestricted";
    
    /**
     * システムプロパティ - JEXLをRESTRICTモードで使用する場合のパーミッションを指定する
     */
    protected static final String[] LIB_PERMISSIONS = {"com.github.mygreen.supercsv.*"};
    
    /**
     * 独自のJEXLのパーミッション。
     */
    protected static final String[] USER_PERMISSIONS;
    static {
        String value = System.getProperty("supercsv.annotation.jexlPermissions");
        if(Utils.isNotEmpty(value)) {
            USER_PERMISSIONS = Arrays.stream(value.split(","))
                    .map(String::trim)
                    .filter(Utils::isNotEmpty)
                    .collect(Collectors.toList())
                    .toArray(new String[0]);
            
        } else {
            USER_PERMISSIONS = new String[] {};
        }
    }
    
    /**
     * JEXLのキャッシュサイズ。
     * <p>キャッシュする式の個数。
     */
    private static final int CACHE_SIZE = 256;
    
    private final JexlEngine jexlEngine;
    
    /**
     * JEXLのパーミッションを指定するコンストラクタ。
     * <p>関数として{@link CustomFunctions}が登録されており、接頭語 {@literal f:}で呼び出し可能です。
     * 
     * @param userPermissions JEXLのパーミッション。
     *        詳細は、<a href="https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/introspection/JexlPermissions.html)">JexlPermissions</a> を参照。
     */
    public ExpressionLanguageJEXLImpl(final String... userPermissions) {
        this(Collections.emptyMap(), true, userPermissions);
        
    }
    
    /**
     * JEXLの独自のEL関数とパーミッションを指定するコンストラクタ。
     * <p>関数として{@link CustomFunctions}が登録されており、接頭語 {@literal f:}で呼び出し可能です。
     * 
     * @param userFunctions 独自のEL関数を指定します。keyは接頭語、valueはメソッドが定義されたクラス。
     * @param userPermissions JEXLのパーミッション。
     *        詳細は、<a href="https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/introspection/JexlPermissions.html)">JexlPermissions</a> を参照。
     */
    public ExpressionLanguageJEXLImpl(final Map<String, Object> userFunctions, final boolean restricted, final String... userPermissions) {
        
        this.jexlEngine = new JexlBuilder()
                .namespaces(buildNamespace(userFunctions))
                .permissions(buildPermissions(restricted, userPermissions))
                .silent(true)
                .cache(CACHE_SIZE)
                .create();
    }
    
    /**
     * {@link JexlEngine}を指定するコンストラクタ。
     * @param jexlEngine JEXLの処理エンジン。
     */
    public ExpressionLanguageJEXLImpl(final JexlEngine jexlEngine) {
        this.jexlEngine = jexlEngine;
    }
    
    /**
     * EL関数の名前空間を組み立て、独自のEL関数を登録します。
     * 
     * @param userFunctions 独自のEL関数を指定します。keyは接頭語、valueはメソッドが定義されたクラス。
     * @return EL関数の名前空間
     */
    protected Map<String, Object> buildNamespace(final Map<String, Object> userFunctions) {
        
        // EL式中で使用可能な関数の登録
        Map<String, Object> functions = new HashMap<>();
        functions.put("f", CustomFunctions.class);
        
        if (Utils.isNotEmpty(userFunctions)) {
            functions.putAll(userFunctions);
        }

        
        return functions;
    }
    
    /**
     * JEXLのパーミッションを組み立てる。
     * 
     * @param userPermissions ユーザー指定のJEXLのパーミッション。
     *        詳細は、<a href="https://commons.apache.org/proper/commons-jexl/apidocs/org/apache/commons/jexl3/introspection/JexlPermissions.html)">JexlPermissions</a> を参照。
     * @return JEXLのパーミッション
     */
    protected JexlPermissions buildPermissions(final boolean restricted, final String... userPermissions) {
        
        final JexlPermissions permissions;
        if(Utils.toBoolean(System.getProperty(PROPERTY_JEXL_RESTRICTED), restricted)) {
            /*
             * EL式で本ライブラリのクラス/メソッドのアクセスを許可する。
             * ・CustomFunctions以外にも、TextPrinterを実装している各CellProcessorでも参照するため。
             * ・JEXL3からサーバーサイド・テンプレート・インジェクション、コマンドインジェクション対策のために、
             *   許可されたクラスしか参照できなくなったため、本ライブラリをEL式から参照可能に許可する。
             */
            String[] concateedUserPermission = Utils.concat(USER_PERMISSIONS, userPermissions);
            permissions = JexlPermissions.RESTRICTED
                    .compose(Utils.concat(LIB_PERMISSIONS, concateedUserPermission));
            
        } else {
            // パーミッションによる制限を行わない。
            permissions = JexlPermissions.UNRESTRICTED;
        }
        
        return permissions;
    }
    
    @Override
    public Object evaluate(final String expression, final Map<String, Object> values) {
        
        Objects.requireNonNull(expression, "expression shoud not be null.");
        Objects.requireNonNull(values, "values shoud not be null.");
        
        if(logger.isDebugEnabled()) {
            logger.debug("Evaluating JEXL expression: {}", expression);
        }
        
        try {
            JexlExpression expr = jexlEngine.createExpression(expression);
            return expr.evaluate(new MapContext((Map<String, Object>) values));
            
        } catch(Exception ex) {
            throw new ExpressionEvaluationException(String.format("Evaluating [%s] script with JEXL failed.", expression), ex,
                    expression, values);
        }
    }
    
    /**
     * {@link JexlEngine}を取得する。
     * @return
     */
    public JexlEngine getJexlEngine() {
        return jexlEngine;
    }
    
}