1. Project Clover database Sat Feb 2 2019 06:45:20 CET
  2. Package org.xwiki.rendering.async.internal

File DefaultAsyncRendererExecutor.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

52
118
10
1
387
253
44
0.37
11.8
10
4.4

Classes

Class Line # Actions
DefaultAsyncRendererExecutor 64 118 0% 44 13
0.9277777792.8%
 

Contributing tests

This file is covered by 16 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.rendering.async.internal;
21   
22    import java.io.Serializable;
23    import java.nio.charset.StandardCharsets;
24    import java.util.ArrayList;
25    import java.util.Collection;
26    import java.util.HashMap;
27    import java.util.List;
28    import java.util.Map;
29    import java.util.TreeMap;
30    import java.util.concurrent.TimeUnit;
31    import java.util.concurrent.atomic.AtomicLong;
32   
33    import javax.inject.Inject;
34    import javax.inject.Named;
35    import javax.inject.Provider;
36    import javax.inject.Singleton;
37   
38    import org.apache.commons.collections4.MapUtils;
39    import org.slf4j.Logger;
40    import org.xwiki.component.annotation.Component;
41    import org.xwiki.component.manager.ComponentLookupException;
42    import org.xwiki.component.manager.ComponentManager;
43    import org.xwiki.context.concurrent.ContextStoreManager;
44    import org.xwiki.job.Job;
45    import org.xwiki.job.JobException;
46    import org.xwiki.job.JobExecutor;
47    import org.xwiki.job.event.status.JobStatus.State;
48    import org.xwiki.rendering.RenderingException;
49    import org.xwiki.rendering.async.AsyncContext;
50    import org.xwiki.rendering.async.AsyncContextHandler;
51    import org.xwiki.rendering.async.internal.DefaultAsyncContext.ContextUse;
52    import org.xwiki.security.authorization.AuthorExecutor;
53   
54    import com.xpn.xwiki.internal.context.XWikiContextContextStore;
55   
56    /**
57    * Default implementation of {@link AsyncRendererExecutor}.
58    *
59    * @version $Id: 376330f6f098f5007e56983a7a196abde547885d $
60    * @since 10.10RC1
61    */
62    @Component
63    @Singleton
 
64    public class DefaultAsyncRendererExecutor implements AsyncRendererExecutor
65    {
66    @Inject
67    @Named(AsyncRendererJobStatus.JOBTYPE)
68    private Provider<Job> jobProvider;
69   
70    @Inject
71    private JobExecutor executor;
72   
73    @Inject
74    private ContextStoreManager contextStore;
75   
76    @Inject
77    private AsyncRendererCache cache;
78   
79    @Inject
80    private AsyncContext asyncContext;
81   
82    @Inject
83    protected AuthorExecutor authorExecutor;
84   
85    @Inject
86    @Named("context")
87    private ComponentManager componentManager;
88   
89    @Inject
90    private Logger logger;
91   
92    private AtomicLong clientIdCount = new AtomicLong();
93   
 
94  2315 toggle @Override
95    public AsyncRendererJobStatus getAsyncStatus(List<String> id, long clientId)
96    {
97    //////////////////////////////////////////////
98    // Try running job
99   
100  2316 Job job = this.executor.getJob(id);
101   
102  2319 if (job != null) {
103  4 return (AsyncRendererJobStatus) job.getStatus();
104    }
105   
106    //////////////////////////////////////////////
107    // Try cache
108   
109  2316 AsyncRendererJobStatus status = this.cache.getAsync(id, clientId);
110   
111  2317 if (status != null) {
112  2317 return status;
113    }
114   
115  0 return null;
116    }
117   
 
118  2317 toggle @Override
119    public AsyncRendererJobStatus getAsyncStatus(List<String> id, long clientId, long time, TimeUnit unit)
120    throws InterruptedException
121    {
122  2317 AsyncRendererJobStatus status = getAsyncStatus(id, clientId);
123   
124  2320 if (status != null && status.getState() != State.FINISHED) {
125  4 Job job = this.executor.getJob(id);
126   
127  4 if (job != null) {
128  4 job.join(time, unit);
129    }
130    }
131   
132  2319 return status;
133    }
134   
 
135  250454 toggle @Override
136    public AsyncRendererExecutorResponse render(AsyncRenderer renderer, AsyncRendererConfiguration configuration)
137    throws JobException, RenderingException
138    {
139  250454 boolean async = renderer.isAsyncAllowed() && this.asyncContext.isEnabled();
140   
141    // Get context and job id
142  250453 Map<String, Serializable> context = getContext(renderer, async, configuration);
143   
144    // Generate job id
145  250454 List<String> jobId = getJobId(renderer, context);
146   
147  250454 if (renderer.isCacheAllowed()) {
148  44022 AsyncRendererJobStatus status = getCurrent(jobId);
149   
150  44022 if (status != null) {
151  30674 if (status.getResult() != null) {
152    // Available cached result, return it
153   
154  30673 injectUses(status);
155   
156  30673 return new AsyncRendererExecutorResponse(status);
157  1 } else if (async) {
158    // Already running job, associate it with another client
159  1 return new AsyncRendererExecutorResponse(status, this.clientIdCount.incrementAndGet());
160    }
161    }
162    }
163   
164    ////////////////////////////////
165    // Execute the renderer
166   
167  219780 AsyncRendererExecutorResponse response;
168   
169  219780 AsyncRendererJobRequest request = new AsyncRendererJobRequest();
170  219780 request.setRenderer(renderer);
171   
172  219780 if (async) {
173  21190 if (context != null) {
174  21190 request.setContext(context);
175    }
176   
177  21191 long asyncClientId = this.clientIdCount.incrementAndGet();
178   
179    // If cache is disabled make sure the id is unique
180  21190 if (!renderer.isCacheAllowed()) {
181  20898 jobId.add(String.valueOf(asyncClientId));
182    }
183   
184  21191 request.setId(jobId);
185   
186  21191 Job job = this.executor.execute(AsyncRendererJobStatus.JOBTYPE, request);
187   
188  21191 AsyncRendererJobStatus status = (AsyncRendererJobStatus) job.getStatus();
189   
190  21191 response = new AsyncRendererExecutorResponse(status, asyncClientId);
191    } else {
192  198589 AsyncRendererJobStatus status;
193   
194    // If async is disabled run the renderer in the current thread
195  198587 if (renderer.isCacheAllowed()) {
196    // Prepare to catch stuff to invalidate the cache
197  13056 if (this.asyncContext instanceof DefaultAsyncContext) {
198  13054 ((DefaultAsyncContext) this.asyncContext).pushContextUse();
199    }
200   
201  13056 AsyncRendererResult result = syncRender(renderer, true, configuration);
202   
203    // Get suff to invalidate the cache
204  13056 if (this.asyncContext instanceof DefaultAsyncContext) {
205  13054 ContextUse contextUse = ((DefaultAsyncContext) this.asyncContext).popContextUse();
206   
207    // Create a pseudo job status
208  13054 status = new AsyncRendererJobStatus(request, result, contextUse.getReferences(),
209    contextUse.getRoleTypes(), contextUse.getRoles(), contextUse.getUses());
210    } else {
211    // Create a pseudo job status
212  2 status = new AsyncRendererJobStatus(request, result, null, null, null, null);
213    }
214   
215  13056 request.setId(jobId);
216   
217  13056 this.cache.put(status);
218    } else {
219  185531 AsyncRendererResult result = syncRender(renderer, false, configuration);
220   
221    // Ceate a pseudo job status
222  185530 status = new AsyncRendererJobStatus(request, result);
223    }
224   
225  198588 response = new AsyncRendererExecutorResponse(status);
226    }
227   
228  219778 return response;
229    }
230   
 
231  198588 toggle private AsyncRendererResult syncRender(AsyncRenderer renderer, boolean cached,
232    AsyncRendererConfiguration configuration) throws RenderingException
233    {
234  198589 if (configuration.isSecureReferenceSet()) {
235  198586 try {
236  198586 return this.authorExecutor.call(() -> renderer.render(false, cached),
237    configuration.getSecureAuthorReference(), configuration.getSecureDocumentReference());
238    } catch (Exception e) {
239  0 throw new RenderingException("Failed to execute renderer", e);
240    }
241    } else {
242  3 return renderer.render(false, cached);
243    }
244    }
245   
 
246  30673 toggle private void injectUses(AsyncRendererJobStatus status)
247    {
248  30673 Map<String, Collection<Object>> uses = status.getUses();
249   
250  30673 if (uses != null) {
251  30673 for (Map.Entry<String, Collection<Object>> entry : uses.entrySet()) {
252  25695 AsyncContextHandler handler;
253  25695 try {
254  25695 handler = this.componentManager.getInstance(AsyncContextHandler.class, entry.getKey());
255    } catch (ComponentLookupException e) {
256  0 this.logger.error("Failed to get AsyncContextHandler with type [{}]", entry.getKey(), e);
257   
258  0 continue;
259    }
260   
261  25695 handler.use(entry.getValue());
262    }
263    }
264    }
265   
 
266  250454 toggle private Map<String, Serializable> getContext(AsyncRenderer renderer, boolean async,
267    AsyncRendererConfiguration configuration) throws JobException
268    {
269  250454 Map<String, Serializable> savedContext = null;
270   
271  250452 if (async || renderer.isCacheAllowed()) {
272  64920 if (configuration.getContextEntries() != null) {
273  64920 try {
274  64920 savedContext = this.contextStore.save(configuration.getContextEntries());
275    } catch (ComponentLookupException e) {
276  0 throw new JobException("Failed to save the context", e);
277    }
278    }
279   
280    // If async inject the configured author
281  64921 if (async && configuration.isSecureReferenceSet()) {
282  41804 if (savedContext == null) {
283  0 savedContext = new HashMap<>();
284    } else {
285    // The map generated by #save might not be modifiable
286  41804 savedContext = new HashMap<>(savedContext);
287    }
288   
289  41804 savedContext.put(XWikiContextContextStore.PROP_SECURE_AUTHOR, configuration.getSecureAuthorReference());
290  41804 savedContext.put(XWikiContextContextStore.PROP_SECURE_DOCUMENT,
291    configuration.getSecureDocumentReference());
292    }
293    }
294   
295  250452 return savedContext;
296    }
297   
 
298  44022 toggle private AsyncRendererJobStatus getCurrent(List<String> jobId)
299    {
300    // Try to find the job status in a running job
301  44022 Job job = this.executor.getJob(jobId);
302   
303    // Found a running job, return it
304  44022 if (job instanceof AsyncRendererJob) {
305  1 return (AsyncRendererJobStatus) job.getStatus();
306    }
307   
308    // Try to find the job status in the cache
309  44021 AsyncRendererJobStatus status = this.cache.getSync(jobId);
310   
311    // Found a cache entry, return it
312  44021 if (status != null) {
313  30673 return status;
314    }
315   
316  13348 return null;
317    }
318   
 
319  250454 toggle private List<String> getJobId(AsyncRenderer renderer, Map<String, Serializable> context)
320    {
321  250454 List<String> rendererId = renderer.getId();
322   
323  250453 if (MapUtils.isEmpty(context)) {
324  185531 return rendererId;
325    }
326   
327    // Order context key to have a reliable job id
328  64921 Map<String, Serializable> orderedMap = new TreeMap<>(context);
329   
330  64921 List<String> id = new ArrayList<>(rendererId.size() + (orderedMap.size() * 2));
331  64920 id.addAll(rendererId);
332  64921 for (Map.Entry<String, Serializable> entry : orderedMap.entrySet()) {
333  258367 id.add(entry.getKey());
334    // We don't really need actual value so let's play it safe regarding values not well supported by
335    // application servers in URLs
336  258369 id.add(encodeId(String.valueOf(entry.getValue())));
337    }
338   
339  64921 return id;
340    }
341   
 
342  258369 toggle private String encodeId(String value)
343    {
344  258369 StringBuilder builder = new StringBuilder(value.length() * 3);
345   
346  4021246 for (int i = 0; i < value.length(); ++i) {
347  3762877 char c = value.charAt(i);
348   
349  3762875 boolean encode = false;
350   
351  3762875 switch (c) {
352    // '/' and '\' has been known to cause issues with various default server setup (Tomcat for example)
353  9 case '/':
354  17 case '\\':
355  26 encode = true;
356   
357  26 break;
358   
359  3762848 default:
360  3762848 break;
361    }
362   
363  3762873 if (encode) {
364  26 encode(c, builder);
365    } else {
366  3762847 builder.append(c);
367    }
368    }
369   
370  258368 return builder.toString();
371    }
372   
 
373  26 toggle private void encode(char c, StringBuilder builder)
374    {
375  26 byte[] ba = String.valueOf(c).getBytes(StandardCharsets.UTF_8);
376   
377  52 for (int j = 0; j < ba.length; j++) {
378  26 builder.append('%');
379   
380  26 char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16);
381  26 builder.append(ch);
382   
383  26 ch = Character.forDigit(ba[j] & 0xF, 16);
384  26 builder.append(ch);
385    }
386    }
387    }