View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.surf.core.scripts;
20  
21  import java.util.LinkedHashMap;
22  import java.util.Map;
23  
24  import org.apache.commons.logging.Log;
25  
26  /**
27   * @author Kevin Roast
28   */
29  public class ScriptResourceHelper
30  {
31      private static final String SCRIPT_ROOT = "_root";
32      private static final String IMPORT_PREFIX = "<import";
33      private static final String IMPORT_RESOURCE = "resource=\"";
34      
35      /**
36       * Resolve the import directives in the specified script. The implementation of the supplied
37       * ScriptResourceLoader instance is responsible for handling the resource retrieval. 
38       * <p>
39       * Multiple includes of the same resource are dealt with correctly and nested includes of scripts
40       * is fully supported.
41       * <p>
42       * Note that for performance reasons the script import directive syntax and placement in the file
43       * is very strict. The import lines <i>must</i> always be first in the file - even before any comments.
44       * Immediately that the script service detects a non-import line it will assume the rest of the
45       * file is executable script and no longer attempt to search for any further import directives. Therefore
46       * all imports should be at the top of the script, one following the other, in the correct syntax and
47       * with no comments present - the only separators valid between import directives is white space.
48       * 
49       * @param script        The script content to resolve imports in
50       * 
51       * @return a valid script with all nested includes resolved into a single script instance 
52       */
53      public static String resolveScriptImports(String script, ScriptResourceLoader loader, Log logger)
54      {
55          // use a linked hashmap to preserve order of includes - the key in the collection is used
56          // to resolve multiple includes of the same scripts and therefore cyclic includes also
57          Map<String, String> scriptlets = new LinkedHashMap<String, String>(8, 1.0f);
58          
59          // perform a recursive resolve of all script imports
60          recurseScriptImports(SCRIPT_ROOT, script, loader, scriptlets, logger);
61          
62          if (scriptlets.size() == 1)
63          {
64              // quick exit for single script with no includes
65              if (logger.isTraceEnabled())
66                  logger.trace("Script content resolved to:\r\n" + script);
67              
68              return script;
69          }
70          else
71          {
72              // calculate total size of buffer required for the script and all includes
73              int length = 0;
74              for (String scriptlet : scriptlets.values())
75              {
76                  length += scriptlet.length();
77              }
78              // append the scripts together to make a single script
79              StringBuilder result = new StringBuilder(length);
80              for (String scriptlet : scriptlets.values())
81              {
82                  result.append(scriptlet);
83              }
84              
85              if (logger.isTraceEnabled())
86                  logger.trace("Script content resolved to:\r\n" + result.toString());
87              
88              return result.toString();
89          }
90      }
91      
92      /**
93       * Recursively resolve imports in the specified scripts, adding the imports to the
94       * specific list of scriplets to combine later.
95       * 
96       * @param location      Script location - used to ensure duplicates are not added
97       * @param script        The script to recursively resolve imports for
98       * @param scripts       The collection of scriplets to execute with imports resolved and removed
99       */
100     private static void recurseScriptImports(
101           String location, String script, ScriptResourceLoader loader, Map<String, String> scripts, Log logger)
102     {
103         int index = 0;
104         // skip any initial whitespace
105         for (; index<script.length(); index++)
106         {
107             if (Character.isWhitespace(script.charAt(index)) == false)
108             {
109                 break;
110             }
111         }
112         // look for the "<import" directive marker
113         if (script.startsWith(IMPORT_PREFIX, index))
114         {
115             // skip whitespace between "<import" and "resource"
116             boolean afterWhitespace = false;
117             index += IMPORT_PREFIX.length() + 1;
118             for (; index<script.length(); index++)
119             {
120                 if (Character.isWhitespace(script.charAt(index)) == false)
121                 {
122                     afterWhitespace = true;
123                     break;
124                 }
125             }
126             if (afterWhitespace == true && script.startsWith(IMPORT_RESOURCE, index))
127             {
128                 // found an import line!
129                 index += IMPORT_RESOURCE.length();
130                 int resourceStart = index;
131                 for (; index<script.length(); index++)
132                 {
133                     if (script.charAt(index) == '"' && script.charAt(index + 1) == '>')
134                     {
135                         // found end of import line - so we have a resource path
136                         String resource = script.substring(resourceStart, index);
137                         
138                         if (logger.isDebugEnabled())
139                             logger.debug("Found script resource import: " + resource);
140                         
141                         if (scripts.containsKey(resource) == false)
142                         {
143                             // load the script resource (and parse any recursive includes...)
144                             String includedScript = loader.loadScriptResource(resource);
145                             if (includedScript != null)
146                             {
147                                 if (logger.isDebugEnabled())
148                                     logger.debug("Succesfully located script '" + resource + "'");
149                                 recurseScriptImports(resource, includedScript, loader, scripts, logger);
150                             }
151                         }
152                         else
153                         {
154                             if (logger.isDebugEnabled())
155                                 logger.debug("Note: already imported resource: " + resource);
156                         }
157                         
158                         // continue scanning this script for additional includes
159                         // skip the last two characters of the import directive
160                         for (index += 2; index<script.length(); index++)
161                         {
162                             if (Character.isWhitespace(script.charAt(index)) == false)
163                             {
164                                 break;
165                             }
166                         }
167                         recurseScriptImports(location, script.substring(index), loader, scripts, logger);
168                         return;
169                     }
170                 }
171                 // if we get here, we failed to find the end of an import line
172                 throw new ScriptException(
173                         "Malformed 'import' line - must be first in file, no comments and strictly of the form:" +
174                         "\r\n<import resource=\"...\">");
175             }
176             else
177             {
178                 throw new ScriptException(
179                         "Malformed 'import' line - must be first in file, no comments and strictly of the form:" +
180                         "\r\n<import resource=\"...\">");
181             }
182         }
183         else
184         {
185             // no (further) includes found - include the original script content
186             if (logger.isDebugEnabled())
187                 logger.debug("Imports resolved, adding resource '" + location);
188             if (logger.isTraceEnabled())
189                 logger.trace(script);
190             scripts.put(location, script);
191         }
192     }
193 }