View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.shardingsphere.infra.expr.groovy;
19  
20  import com.github.benmanes.caffeine.cache.Cache;
21  import com.github.benmanes.caffeine.cache.Caffeine;
22  import com.google.common.base.Strings;
23  import com.google.common.collect.Sets;
24  import groovy.lang.Closure;
25  import groovy.lang.GString;
26  import groovy.lang.GroovyShell;
27  import groovy.lang.Script;
28  import groovy.util.Expando;
29  import org.apache.shardingsphere.infra.config.props.ConfigurationProperties;
30  import org.apache.shardingsphere.infra.config.props.ConfigurationPropertyKey;
31  import org.apache.shardingsphere.infra.expr.core.GroovyUtils;
32  import org.apache.shardingsphere.infra.expr.spi.InlineExpressionParser;
33  
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collection;
37  import java.util.Collections;
38  import java.util.LinkedHashSet;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Properties;
43  import java.util.Set;
44  import java.util.stream.Collectors;
45  
46  /**
47   * Groovy inline expression parser.
48   */
49  public final class GroovyInlineExpressionParser implements InlineExpressionParser {
50      
51      private static final String INLINE_EXPRESSION_KEY = "inlineExpression";
52      
53      private static final GroovyShell SHELL = new GroovyShell();
54      
55      private static volatile long currentCacheSize = Long.parseLong(ConfigurationPropertyKey.GROOVY_INLINE_EXPRESSION_PARSING_CACHE_MAX_SIZE.getDefaultValue());
56      
57      private static volatile Cache<String, Script> scriptCache = Caffeine.newBuilder().maximumSize(currentCacheSize).softValues().build();
58      
59      private String inlineExpression;
60      
61      @Override
62      public void init(final Properties props) {
63          inlineExpression = props.getProperty(INLINE_EXPRESSION_KEY);
64          long maxCacheSize = new ConfigurationProperties(props).getValue(ConfigurationPropertyKey.GROOVY_INLINE_EXPRESSION_PARSING_CACHE_MAX_SIZE);
65          updateMaxCacheSize(maxCacheSize);
66      }
67      
68      private static void updateMaxCacheSize(final long newMaxCacheSize) {
69          if (newMaxCacheSize == currentCacheSize) {
70              return;
71          }
72          synchronized (GroovyInlineExpressionParser.class) {
73              if (newMaxCacheSize != currentCacheSize) {
74                  scriptCache = Caffeine.newBuilder().maximumSize(newMaxCacheSize).softValues().build();
75                  currentCacheSize = newMaxCacheSize;
76              }
77          }
78      }
79      
80      @Override
81      public String handlePlaceHolder() {
82          return handlePlaceHolder(inlineExpression);
83      }
84      
85      /**
86       * Replace all inline expression placeholders.
87       *
88       * @param inlineExpression inline expression with {@code $->}
89       * @return result inline expression with {@code $}
90       */
91      private String handlePlaceHolder(final String inlineExpression) {
92          return inlineExpression.contains("$->{") ? inlineExpression.replaceAll("\\$->\\{", "\\${") : inlineExpression;
93      }
94      
95      /**
96       * Split and Evaluate inline expression. This function will replace all inline expression placeholders.
97       *
98       * @return result inline expression with {@code $}
99       */
100     @Override
101     public List<String> splitAndEvaluate() {
102         if (Strings.isNullOrEmpty(inlineExpression)) {
103             return Collections.emptyList();
104         }
105         if (isConstantExpression(inlineExpression)) {
106             return Arrays.stream(inlineExpression.split("\\s*,\\s*")).map(String::trim).filter(each -> !each.isEmpty()).collect(Collectors.toList());
107         }
108         return flatten(evaluate(GroovyUtils.split(handlePlaceHolder(inlineExpression))));
109     }
110     
111     /**
112      * Turn inline expression into Groovy Closure. This function will replace all inline expression placeholders.
113      * For compatibility reasons, it does not check whether the unit of the input parameter map is null.
114      * @return The result of the Groovy Closure pattern.
115      */
116     @Override
117     public String evaluateWithArgs(final Map<String, Comparable<?>> map) {
118         if (isConstantExpression(inlineExpression)) {
119             return inlineExpression;
120         }
121         Object scriptResult = evaluate("{it -> \"" + handlePlaceHolder(inlineExpression) + "\"}");
122         if (scriptResult instanceof Closure) {
123             Closure<?> result = ((Closure<?>) scriptResult).rehydrate(new Expando(), null, null);
124             result.setResolveStrategy(Closure.DELEGATE_ONLY);
125             map.forEach(result::setProperty);
126             return result.call().toString();
127         }
128         return scriptResult.toString();
129     }
130     
131     private List<Object> evaluate(final List<String> inlineExpressions) {
132         List<Object> result = new ArrayList<>(inlineExpressions.size());
133         for (String each : inlineExpressions) {
134             StringBuilder expression = new StringBuilder(handlePlaceHolder(each));
135             if (!each.startsWith("\"")) {
136                 expression.insert(0, '"');
137             }
138             if (!each.endsWith("\"")) {
139                 expression.append('"');
140             }
141             result.add(evaluate(expression.toString()));
142         }
143         return result;
144     }
145     
146     private Object evaluate(final String expression) {
147         if (isConstantExpression(expression)) {
148             return expression.replaceAll("^\"|\"$", "");
149         }
150         Script script = scriptCache.get(expression, SHELL::parse);
151         return null == script ? expression : script.run();
152     }
153     
154     private boolean isConstantExpression(final String expression) {
155         return Strings.isNullOrEmpty(expression) || !isDynamicExpression(expression);
156     }
157     
158     private boolean isDynamicExpression(final String expression) {
159         return expression.contains("${") || expression.contains("$->{");
160     }
161     
162     private List<String> flatten(final List<Object> segments) {
163         List<String> result = new ArrayList<>();
164         for (Object each : segments) {
165             if (each instanceof GString) {
166                 result.addAll(assemblyCartesianSegments((GString) each));
167             } else if (each instanceof Script) {
168                 result.addAll(flattenScript((Script) each));
169             } else {
170                 result.add(each.toString());
171             }
172         }
173         return result;
174     }
175     
176     private List<String> assemblyCartesianSegments(final GString segment) {
177         Set<List<String>> cartesianValues = getCartesianValues(segment);
178         List<String> result = new ArrayList<>(cartesianValues.size());
179         for (List<String> each : cartesianValues) {
180             result.add(assemblySegment(each, segment));
181         }
182         return result;
183     }
184     
185     @SuppressWarnings("unchecked")
186     private Set<List<String>> getCartesianValues(final GString segment) {
187         List<Set<String>> result = new ArrayList<>(segment.getValues().length);
188         for (Object each : segment.getValues()) {
189             if (null == each) {
190                 continue;
191             }
192             if (each instanceof Collection) {
193                 result.add(((Collection<Object>) each).stream().map(Object::toString).collect(Collectors.toCollection(LinkedHashSet::new)));
194             } else {
195                 result.add(Sets.newHashSet(each.toString()));
196             }
197         }
198         return Sets.cartesianProduct(result);
199     }
200     
201     private String assemblySegment(final List<String> cartesianValue, final GString segment) {
202         StringBuilder result = new StringBuilder();
203         for (int i = 0; i < segment.getStrings().length; i++) {
204             result.append(segment.getStrings()[i]);
205             if (i < cartesianValue.size()) {
206                 result.append(cartesianValue.get(i));
207             }
208         }
209         return result.toString();
210     }
211     
212     private Collection<String> flattenScript(final Script script) {
213         Collection<String> result = new LinkedList<>();
214         Object scriptResult = script.run();
215         if (scriptResult instanceof Iterable) {
216             for (Object item : (Iterable<?>) scriptResult) {
217                 result.add(item.toString());
218             }
219         } else {
220             result.add(scriptResult.toString());
221         }
222         return result;
223     }
224     
225     @Override
226     public String getType() {
227         return "GROOVY";
228     }
229 }