View Javadoc
1   /* 
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   *
14   */
15  
16  package org.esigate.extension.parallelesi;
17  
18  import java.io.IOException;
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Map.Entry;
24  import java.util.concurrent.Callable;
25  import java.util.concurrent.Executor;
26  import java.util.concurrent.Future;
27  import java.util.concurrent.FutureTask;
28  import java.util.concurrent.RejectedExecutionException;
29  import java.util.concurrent.RunnableFuture;
30  import java.util.regex.Pattern;
31  
32  import org.apache.commons.io.output.StringBuilderWriter;
33  import org.apache.http.client.methods.CloseableHttpResponse;
34  import org.esigate.ConfigurationException;
35  import org.esigate.Driver;
36  import org.esigate.DriverFactory;
37  import org.esigate.HttpErrorPage;
38  import org.esigate.Parameters;
39  import org.esigate.Renderer;
40  import org.esigate.http.HttpResponseUtils;
41  import org.esigate.impl.DriverRequest;
42  import org.esigate.parser.future.CharSequenceFuture;
43  import org.esigate.parser.future.FutureElement;
44  import org.esigate.parser.future.FutureElementType;
45  import org.esigate.parser.future.FutureParserContext;
46  import org.esigate.parser.future.StringBuilderFutureAppendable;
47  import org.esigate.xml.XpathRenderer;
48  import org.esigate.xml.XsltRenderer;
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  class IncludeElement extends BaseElement {
53      private static final String PROVIDER_PATTERN = "$(PROVIDER{";
54      private static final String LEGACY_PROVIDER_PATTERN = "$PROVIDER({";
55  
56      private static final Logger LOG = LoggerFactory.getLogger(IncludeElement.class);
57  
58      private static final class IncludeTask implements Callable<CharSequence> {
59          private String src;
60          private String alt;
61          private FutureParserContext ctx;
62          private boolean ignoreError;
63          private FutureElement current;
64          private Tag includeTag;
65          private Map<String, CharSequence> fragmentReplacements;
66          private Map<String, CharSequence> regexpReplacements;
67          private Executor executor;
68  
69          private IncludeTask(Tag includeTag, String src, String alt, FutureParserContext ctx, FutureElement current,
70                  boolean ignoreError, Map<String, CharSequence> fragmentReplacements,
71                  Map<String, CharSequence> regexpReplacements, Executor executor) {
72              this.src = src;
73              this.alt = alt;
74              this.ctx = ctx;
75              this.ignoreError = ignoreError;
76              this.current = current;
77              this.includeTag = includeTag;
78              this.fragmentReplacements = fragmentReplacements;
79              this.regexpReplacements = regexpReplacements;
80              this.executor = executor;
81          }
82  
83          @Override
84          public CharSequence call() throws IOException, HttpErrorPage {
85              LOG.debug("Starting include task {}", this.src);
86              StringBuilderWriter sw = new StringBuilderWriter(Parameters.DEFAULT_BUFFER_SIZE);
87  
88              Exception currentException = null;
89              // Handle src
90              try {
91                  processPage(this.src, includeTag, sw);
92              } catch (IOException | HttpErrorPage e) {
93                  currentException = e;
94              } catch (ConfigurationException e) {
95                  // case uknown provider : log error
96                  currentException = e;
97                  LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
98              }
99  
100             // Handle Alt
101             if (currentException != null && alt != null) {
102                 // Reset exception
103                 currentException = null;
104                 try {
105                     processPage(alt, includeTag, sw);
106                 } catch (IOException | HttpErrorPage e) {
107                     currentException = e;
108                 } catch (ConfigurationException e) {
109                     // case uknown provider : log error
110                     currentException = e;
111                     LOG.error("Esi Include Tag with unknown Provider :" + e.getMessage());
112                 }
113             }
114 
115             // Handle onerror
116             if (currentException != null && !ignoreError && !ctx.reportError(current, currentException)) {
117                 if (currentException instanceof IOException) {
118                     throw (IOException) currentException;
119                 } else if (currentException instanceof HttpErrorPage) {
120                     throw (HttpErrorPage) currentException;
121                 } else if (currentException instanceof ConfigurationException) {
122                     throw (ConfigurationException) currentException;
123                 }
124                 throw new IllegalStateException(
125                         "This type of exception is unexpected here. Should be IOException or HttpErrorPageException or ConfigurationException.",
126                         currentException);
127             }
128 
129             // apply regexp replacements
130             String result = sw.toString();
131 
132             if (!regexpReplacements.isEmpty()) {
133                 for (Entry<String, CharSequence> entry : regexpReplacements.entrySet()) {
134 
135                     result = Pattern.compile(entry.getKey()).matcher(result).replaceAll(entry.getValue().toString());
136                 }
137             }
138 
139             return result;
140         }
141 
142         private void processPage(String srcOrAlt, Tag tag, Appendable out) throws IOException, HttpErrorPage {
143             String fragment = tag.getAttribute("fragment");
144             String xpath = tag.getAttribute("xpath");
145             String xslt = tag.getAttribute("stylesheet");
146 
147             DriverRequest httpRequest = ctx.getHttpRequest();
148             List<Renderer> rendererList = new ArrayList<>();
149             Driver driver;
150             String page;
151 
152             int idx = srcOrAlt.indexOf(PROVIDER_PATTERN);
153             int idxLegacyPattern = srcOrAlt.indexOf(LEGACY_PROVIDER_PATTERN);
154             if (idx < 0 && idxLegacyPattern < 0) {
155                 page = srcOrAlt;
156                 driver = httpRequest.getDriver();
157             } else if (idx >= 0) {
158                 int startIdx = idx + PROVIDER_PATTERN.length();
159                 int endIndex = srcOrAlt.indexOf("})", startIdx);
160                 String provider = srcOrAlt.substring(startIdx, endIndex);
161                 page = srcOrAlt.substring(endIndex + "})".length());
162                 driver = DriverFactory.getInstance(provider);
163                 if (LOG.isWarnEnabled() && idx > 0) {
164                     LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
165                             + " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
166                             srcOrAlt.substring(0, idx));
167                 }
168             } else {
169                 int startIdx = idxLegacyPattern + PROVIDER_PATTERN.length();
170                 int endIndex = srcOrAlt.indexOf("})", startIdx);
171                 String provider = srcOrAlt.substring(startIdx, endIndex);
172                 page = srcOrAlt.substring(endIndex + "})".length());
173                 driver = DriverFactory.getInstance(provider);
174                 if (LOG.isWarnEnabled() && idxLegacyPattern > 0) {
175                     LOG.warn("Invalid src attribute : [{}], src should start with [{}{}})]."
176                             + " First characters [{}] have been ignored", srcOrAlt, PROVIDER_PATTERN, provider,
177                             srcOrAlt.substring(0, idxLegacyPattern));
178                 }
179             }
180 
181             InlineCache ic = InlineCache.getFragment(srcOrAlt);
182             if (ic != null && !ic.isExpired()) {
183                 String cache = ic.getFragment();
184                 out.append(cache);
185             } else {
186                 EsiRenderer esiRenderer;
187                 if (fragment != null) {
188                     esiRenderer = new EsiRenderer(page, fragment, executor);
189                 } else {
190                     esiRenderer = new EsiRenderer(executor);
191                 }
192                 if (fragmentReplacements != null && !fragmentReplacements.isEmpty()) {
193                     esiRenderer.setFragmentsToReplace(fragmentReplacements);
194                 }
195                 rendererList.add(esiRenderer);
196                 if (xpath != null) {
197                     rendererList.add(new XpathRenderer(xpath));
198                 } else if (xslt != null) {
199                     rendererList.add(new XsltRenderer(xslt, driver, httpRequest));
200                 }
201                 CloseableHttpResponse response =
202                         driver.render(page, httpRequest.getOriginalRequest(),
203                                 rendererList.toArray(new Renderer[rendererList.size()]));
204                 out.append(HttpResponseUtils.toString(response));
205             }
206         }
207 
208     }
209 
210     public static final FutureElementType TYPE = new BaseElementType("<esi:include", "</esi:include") {
211         @Override
212         public IncludeElement newInstance() {
213             return new IncludeElement();
214         }
215 
216     };
217 
218     private StringBuilderFutureAppendable buf;
219     private Map<String, CharSequence> fragmentReplacements;
220     private Map<String, CharSequence> regexpReplacements;
221     private Tag includeTag;
222     private boolean write = false;
223 
224     IncludeElement() {
225     }
226 
227     @Override
228     public void characters(Future<CharSequence> csq) {
229         if (write) {
230             buf.enqueueAppend(csq);
231         }
232     }
233 
234     @Override
235     public void onTagEnd(String tag, FutureParserContext ctx) throws IOException, HttpErrorPage {
236         write = true;
237         String src = includeTag.getAttribute("src");
238         String alt = includeTag.getAttribute("alt");
239         boolean ignoreError = "continue".equals(includeTag.getAttribute("onerror"));
240         FutureElement current = ctx.getCurrent();
241         // write accumulated data into parent
242         Executor executor = (Executor) ctx.getData(EsiRenderer.DATA_EXECUTOR);
243         Future<CharSequence> result;
244         IncludeTask task =
245                 new IncludeTask(includeTag, src, alt, ctx, current, ignoreError, fragmentReplacements,
246                         regexpReplacements, null); // executor is null to disable parallel esi on recursive calls.
247         if (executor == null) {
248             // No threads.
249             CharSequence content = task.call();
250             result = new CharSequenceFuture(content);
251         } else {
252             // Start processing in a new thread.
253             try {
254                 RunnableFuture<CharSequence> r = new FutureTask<>(task);
255                 executor.execute(r);
256                 result = r;
257             } catch (RejectedExecutionException e) {
258                 throw new HttpErrorPage(509, "Limits exceeded", e);
259             }
260         }
261         ctx.getCurrent().characters(result);
262     }
263 
264     @Override
265     protected boolean parseTag(Tag tag, FutureParserContext ctx) {
266         buf = new StringBuilderFutureAppendable();
267         fragmentReplacements = new HashMap<>();
268         regexpReplacements = new HashMap<>();
269         includeTag = tag;
270         return true;
271     }
272 
273     void addFragmentReplacement(String fragment, CharSequence replacement) {
274         fragmentReplacements.put(fragment, replacement);
275     }
276 
277     void addRegexpReplacement(String regexp, CharSequence replacement) {
278         regexpReplacements.put(regexp, replacement);
279     }
280 
281 }