View Javadoc
1   package org.apache.struts.scripting;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.InputStreamReader;
6   import java.io.Reader;
7   import java.nio.charset.StandardCharsets;
8   import java.nio.file.Files;
9   import java.nio.file.Path;
10  import java.nio.file.Paths;
11  import java.nio.file.attribute.FileTime;
12  
13  import javax.script.Bindings;
14  import javax.script.Compilable;
15  import javax.script.CompiledScript;
16  import javax.script.ScriptContext;
17  import javax.script.ScriptEngine;
18  import javax.script.ScriptEngineManager;
19  import javax.script.ScriptException;
20  
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  
24  import jakarta.servlet.ServletContext;
25  
26  /**
27   * Represents a saved script.
28   *
29   * <p>This class is thread-safe.</p>
30   *
31   * @author Stefan Graff
32   *
33   * @since Struts 1.4.1
34   */
35  class Script {
36  
37      /**
38       * The {@code Log} instance for this class.
39       */
40      private final Logger log =
41          LoggerFactory.getLogger(Script.class);
42  
43      /**  The name of the script file. */
44      public final String name;
45  
46      /**
47       * The script file as path. If {@code null} the
48       * script file was read thru {@code InputStream}.
49       */
50      public final Path path;
51  
52      /**  The ScriptEngine for the script. */
53      public final ScriptEngine scriptEngine;
54  
55      /** Indicator if the script-engine is compilable */
56      public final boolean compilable;
57  
58      /**  The time when the script was last modified. */
59      public FileTime lastModifiedTime;
60  
61      /**  The contents of the script file. */
62      public String content;
63  
64      /**  The compiled script if it is possible from the script-engine. */
65      public CompiledScript compiledScript;
66  
67      /** Saves the last io-exception. */
68      private IOException ioe;
69  
70      /** Saves the last script-exception. */
71      private ScriptException se;
72  
73      /**
74       * Creates a new instance of this class.
75       *
76       * <p>A possible {@code ScriptException} and/or
77       * {@code IOException} will be saved.</p>
78       *
79       * @param scriptEngineManager The entry-point to JSR223-scripting
80       * @param context             The servlet context
81       * @param name                The name of the script file
82       */
83      public Script(final ScriptEngineManager scriptEngineManager,
84              final ServletContext context, final String name) {
85  
86          this.name = name;
87  
88          final int i = name.lastIndexOf('.');
89          final String ext = i < 0 ? name : name.substring(i + 1);
90          scriptEngine = scriptEngineManager.getEngineByExtension(ext);
91          if (scriptEngine == null) {
92              se = new ScriptException("No ScriptEngine found for file: " + name);
93          }
94  
95          compilable = scriptEngine instanceof Compilable;
96  
97          final String realPath = context.getRealPath(name);
98          if (realPath == null) {
99              path = null;
100         } else {
101             path = Paths.get(realPath);
102         }
103         
104         try (InputStream inputStream = realPath == null
105                 ? context.getResourceAsStream(name)
106                 : null) {
107 
108             if (path == null && inputStream == null) {
109                 se = new ScriptException("Could not find resource for file: " + name);
110             }
111 
112             if (se != null) {
113                 return;
114             }
115 
116             try (Reader r = path == null
117                     ? new InputStreamReader(inputStream, StandardCharsets.UTF_8)
118                     : Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
119 
120                 lastModifiedTime = path != null
121                         ? IOUtils.getLastModifiedTime(path)
122                         : null;
123 
124                 log.debug("Loading new script: {}", name);
125 
126                 setContent(IOUtils.getStringFromReader(r));
127             }
128         } catch (IOException e) {
129             ioe = e;
130         }
131     }
132 
133     /**
134      * Checks the script file if it has a new content. If so, the
135      * new content is loaded and compiled if possible.
136      *
137      * <p>A possible {@code ScriptException} and/or
138      * {@code IOException} will be saved.</p>
139      *
140      * @return {@code true} script file is updated
141      */
142     public boolean checkNewContent() {
143         this.ioe = null;
144 
145         if (path == null) {
146             return false;
147         }
148 
149         try {
150             final FileTime lastModifiedTime = IOUtils.getLastModifiedTime(path);
151             if (this.lastModifiedTime != null &&
152                     this.lastModifiedTime.compareTo(lastModifiedTime) >= 0) {
153 
154                 return false;
155             }
156 
157             synchronized (this) {
158                 log.debug("Loading updated script: {}", name);
159 
160                 this.lastModifiedTime = lastModifiedTime;
161 
162                 try (Reader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
163                     setContent(IOUtils.getStringFromReader(r));
164                 }
165             }
166         } catch (IOException e) {
167             ioe = e;
168         }
169 
170         return true;
171     }
172 
173     /**
174      * Returns a set of names values representing the state of this script.
175      * The values are generally visible in this scripts using the associated
176      * keys as variable names.
177      *
178      * @return a scope of named values
179      */
180     public Bindings getBindings() {
181         final Bindings bindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
182         bindings.clear();
183         bindings.put(ScriptEngine.FILENAME, name);
184 
185         return bindings;
186     }
187 
188     /**
189      * Checks if an {@code ScriptException} and/or
190      * {@code IOException} is saved.
191      *
192      * @throws IOException if an I/O error occurs
193      * @throws ScriptException if compilation fails
194      */
195     public void checkExceptions() throws ScriptException, IOException {
196         if (ioe != null) {
197             throw ioe;
198         }
199 
200         if (se != null) {
201             throw se;
202         }
203     }
204 
205     /**
206      * Executes the specified script. The default
207      * {@code ScriptContext} for this {@code Script} is used.
208      *
209      * @return The value returned from the execution of the script.
210      * @throws ScriptException if error occurs in script.
211      */
212     public Object eval() throws ScriptException {
213         if (compilable) {
214             if (compiledScript == null) {
215                 throw se == null ? new ScriptException("Script could not compiled") : se;
216             }
217             return compiledScript.eval();
218         }
219 
220         if (content == null || content.isEmpty()) {
221             throw se == null ? new ScriptException("Script could not compiled") : se;
222         }
223         return scriptEngine.eval(content);
224     }
225 
226     /**
227      * Compiles the {@code Content} if it is possible.
228      *
229      * <p>A possible {@code ScriptException} will be saved.</p>
230      *
231      * @param content The {@code Content} to compile.
232      */
233     private void setContent(final String content) {
234         this.se = null;
235         this.compiledScript = null;
236 
237         if (content == null || content.isEmpty()) {
238             this.content = "";
239         } else if (compilable) {
240             this.content = null;
241 
242             try {
243                 this.compiledScript = ((Compilable) scriptEngine).compile(content);
244             } catch (ScriptException e) {
245                 se = e;
246             }
247         } else {
248             this.content = content;
249         }
250     }
251 }