1 | /** |
---|
2 | * This software is copyright (c) 2013 by |
---|
3 | * - Institut fuer Deutsche Sprache (http://www.ids-mannheim.de) |
---|
4 | * This is free software. You can redistribute it |
---|
5 | * and/or modify it under the terms described in |
---|
6 | * the GNU General Public License v3 of which you |
---|
7 | * should have received a copy. Otherwise you can download |
---|
8 | * it from |
---|
9 | * |
---|
10 | * http://www.gnu.org/licenses/gpl-3.0.txt |
---|
11 | * |
---|
12 | * @copyright Institut fuer Deutsche Sprache (http://www.ids-mannheim.de) |
---|
13 | * |
---|
14 | * @license http://www.gnu.org/licenses/gpl-3.0.txt |
---|
15 | * GNU General Public License v3 |
---|
16 | */ |
---|
17 | package eu.clarin.fcs.tester; |
---|
18 | |
---|
19 | import java.io.IOException; |
---|
20 | import java.util.ArrayList; |
---|
21 | import java.util.Arrays; |
---|
22 | import java.util.Collections; |
---|
23 | import java.util.Comparator; |
---|
24 | import java.util.LinkedList; |
---|
25 | import java.util.List; |
---|
26 | import java.util.Set; |
---|
27 | import java.util.concurrent.ExecutorService; |
---|
28 | import java.util.concurrent.Executors; |
---|
29 | import java.util.concurrent.RejectedExecutionException; |
---|
30 | import java.util.concurrent.TimeUnit; |
---|
31 | import java.util.logging.Handler; |
---|
32 | import java.util.logging.Level; |
---|
33 | import java.util.logging.LogRecord; |
---|
34 | |
---|
35 | import javax.servlet.ServletContextEvent; |
---|
36 | import javax.servlet.ServletContextListener; |
---|
37 | |
---|
38 | import org.apache.http.HttpResponse; |
---|
39 | import org.apache.http.HttpStatus; |
---|
40 | import org.apache.http.StatusLine; |
---|
41 | import org.apache.http.client.ClientProtocolException; |
---|
42 | import org.apache.http.client.config.CookieSpecs; |
---|
43 | import org.apache.http.client.config.RequestConfig; |
---|
44 | import org.apache.http.client.methods.HttpHead; |
---|
45 | import org.apache.http.client.protocol.HttpClientContext; |
---|
46 | import org.apache.http.client.utils.HttpClientUtils; |
---|
47 | import org.apache.http.config.SocketConfig; |
---|
48 | import org.apache.http.impl.NoConnectionReuseStrategy; |
---|
49 | import org.apache.http.impl.client.CloseableHttpClient; |
---|
50 | import org.apache.http.impl.client.HttpClients; |
---|
51 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; |
---|
52 | import org.reflections.Reflections; |
---|
53 | import org.slf4j.Logger; |
---|
54 | import org.slf4j.LoggerFactory; |
---|
55 | |
---|
56 | import eu.clarin.sru.client.SRUClient; |
---|
57 | import eu.clarin.sru.client.SRUClientConfig; |
---|
58 | import eu.clarin.sru.client.SRUClientException; |
---|
59 | import eu.clarin.sru.client.SRUExplainRequest; |
---|
60 | import eu.clarin.sru.client.SRUExplainResponse; |
---|
61 | import eu.clarin.sru.client.SRUVersion; |
---|
62 | import eu.clarin.sru.client.fcs.ClarinFCSClientBuilder; |
---|
63 | import eu.clarin.sru.client.fcs.ClarinFCSConstants; |
---|
64 | import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescription; |
---|
65 | import eu.clarin.sru.client.fcs.ClarinFCSEndpointDescriptionParser; |
---|
66 | |
---|
67 | |
---|
68 | public class FCSEndpointTester implements ServletContextListener { |
---|
69 | public interface ProgressListener { |
---|
70 | public void updateProgress(String message); |
---|
71 | |
---|
72 | public void updateMaximum(int maximum); |
---|
73 | |
---|
74 | public void onDone(FCSTestContext context, List<FCSTestResult> results); |
---|
75 | |
---|
76 | public void onError(String message, Throwable e); |
---|
77 | } |
---|
78 | private static final String TESTCASE_PACKAGE = |
---|
79 | "eu.clarin.fcs.tester.tests"; |
---|
80 | private static final String USER_AGENT = "FCSEndpointTester/1.0.0"; |
---|
81 | private static final int DEFAULT_CONNECT_TIMEOUT = -1; |
---|
82 | private static final int DEFAULT_SOCKET_TIMEOUT = -1; |
---|
83 | private static final List<FCSTest> TESTS; |
---|
84 | private static final Logger logger = |
---|
85 | LoggerFactory.getLogger(FCSEndpointTester.class); |
---|
86 | private final FCSLoggingHandler logcapturehandler = |
---|
87 | new FCSLoggingHandler(); |
---|
88 | private final ExecutorService executor = Executors.newCachedThreadPool(); |
---|
89 | private static final FCSEndpointTester INSTANCE = new FCSEndpointTester(); |
---|
90 | |
---|
91 | |
---|
92 | public static FCSEndpointTester getInstance() { |
---|
93 | return INSTANCE; |
---|
94 | } |
---|
95 | |
---|
96 | |
---|
97 | private void destroy() { |
---|
98 | executor.shutdownNow(); |
---|
99 | try { |
---|
100 | executor.awaitTermination(5000, TimeUnit.MILLISECONDS); |
---|
101 | logger.debug("thread pool terminated"); |
---|
102 | } catch (InterruptedException e) { |
---|
103 | /* IGNORE */ |
---|
104 | } |
---|
105 | } |
---|
106 | |
---|
107 | |
---|
108 | private static CloseableHttpClient createHttpClient(int connectTimeout, |
---|
109 | int socketTimeout) { |
---|
110 | final PoolingHttpClientConnectionManager manager = |
---|
111 | new PoolingHttpClientConnectionManager(); |
---|
112 | manager.setDefaultMaxPerRoute(8); |
---|
113 | manager.setMaxTotal(128); |
---|
114 | |
---|
115 | final SocketConfig socketConfig = SocketConfig.custom() |
---|
116 | .setSoReuseAddress(true) |
---|
117 | .setSoLinger(0) |
---|
118 | .build(); |
---|
119 | |
---|
120 | final RequestConfig requestConfig = RequestConfig.custom() |
---|
121 | .setAuthenticationEnabled(false) |
---|
122 | .setRedirectsEnabled(true) |
---|
123 | .setMaxRedirects(4) |
---|
124 | .setCircularRedirectsAllowed(false) |
---|
125 | .setCookieSpec(CookieSpecs.IGNORE_COOKIES) |
---|
126 | .setConnectTimeout(connectTimeout) |
---|
127 | .setSocketTimeout(socketTimeout) |
---|
128 | .setConnectionRequestTimeout(0) /* infinite */ |
---|
129 | .build(); |
---|
130 | |
---|
131 | return HttpClients.custom() |
---|
132 | .setUserAgent(USER_AGENT) |
---|
133 | .setConnectionManager(manager) |
---|
134 | .setDefaultSocketConfig(socketConfig) |
---|
135 | .setDefaultRequestConfig(requestConfig) |
---|
136 | .setConnectionReuseStrategy(new NoConnectionReuseStrategy()) |
---|
137 | .build(); |
---|
138 | } |
---|
139 | |
---|
140 | |
---|
141 | public void performTests(final ProgressListener listener, |
---|
142 | final FCSTestProfile profile, |
---|
143 | final String endpointURI, |
---|
144 | final String searchTerm, |
---|
145 | final boolean strictMode, |
---|
146 | final boolean performProbeQuest, |
---|
147 | final int connectTimeout, |
---|
148 | final int socketTimeout) { |
---|
149 | if (listener == null) { |
---|
150 | throw new NullPointerException("listener == null"); |
---|
151 | } |
---|
152 | try { |
---|
153 | executor.submit(new Runnable() { |
---|
154 | @Override |
---|
155 | public void run() { |
---|
156 | try { |
---|
157 | FCSTestContext context = null; |
---|
158 | if (performProbeQuest) { |
---|
159 | doPerformProbeRequest(listener, endpointURI); |
---|
160 | } |
---|
161 | // auto-detect FCS version? |
---|
162 | if (profile == null) { |
---|
163 | context = doPerformAutodetect(listener, |
---|
164 | endpointURI, |
---|
165 | searchTerm, |
---|
166 | strictMode, |
---|
167 | connectTimeout, |
---|
168 | socketTimeout); |
---|
169 | } else { |
---|
170 | context = new FCSTestContext(profile, |
---|
171 | endpointURI, |
---|
172 | searchTerm, |
---|
173 | strictMode, |
---|
174 | connectTimeout, |
---|
175 | socketTimeout); |
---|
176 | } |
---|
177 | context.init(); |
---|
178 | doPerformTests(listener, context); |
---|
179 | } catch (IOException e) { |
---|
180 | listener.onError("An error occurred", e); |
---|
181 | } catch (SRUClientException e) { |
---|
182 | listener.onError(e.getMessage(), e.getCause()); |
---|
183 | } catch (Throwable t) { |
---|
184 | logger.error("Internal error!", t); |
---|
185 | listener.onError("Internal error!", t); |
---|
186 | } |
---|
187 | } |
---|
188 | }); |
---|
189 | } catch (RejectedExecutionException e) { |
---|
190 | listener.onError("Error starting tests", null); |
---|
191 | } |
---|
192 | } |
---|
193 | |
---|
194 | |
---|
195 | private FCSTestContext doPerformAutodetect(final ProgressListener listener, |
---|
196 | final String endpointURI, |
---|
197 | final String searchTerm, |
---|
198 | final boolean strictMode, |
---|
199 | final int connectTimeout, |
---|
200 | final int socketTimeout) throws SRUClientException { |
---|
201 | listener.updateProgress("Detecting CLARIN-FCS profile ..."); |
---|
202 | |
---|
203 | FCSTestProfile profile = null; |
---|
204 | |
---|
205 | SRUClient client = new ClarinFCSClientBuilder() |
---|
206 | .addDefaultDataViewParsers() |
---|
207 | .setDefaultSRUVersion(SRUVersion.VERSION_2_0) |
---|
208 | .unknownDataViewAsString() |
---|
209 | .enableLegacySupport() |
---|
210 | .registerExtraResponseDataParser( |
---|
211 | new ClarinFCSEndpointDescriptionParser()) |
---|
212 | .buildClient(); |
---|
213 | |
---|
214 | try { |
---|
215 | SRUExplainRequest request = new SRUExplainRequest(endpointURI); |
---|
216 | request.setStrictMode(false); |
---|
217 | request.setVersion(SRUVersion.VERSION_1_2); |
---|
218 | request.setExtraRequestData(ClarinFCSConstants.X_FCS_ENDPOINT_DESCRIPTION, |
---|
219 | ClarinFCSConstants.TRUE); |
---|
220 | request.setParseRecordDataEnabled(true); |
---|
221 | SRUExplainResponse response = client.explain(request); |
---|
222 | |
---|
223 | ClarinFCSEndpointDescription ed = |
---|
224 | response.getFirstExtraResponseData(ClarinFCSEndpointDescription.class); |
---|
225 | if (ed != null) { |
---|
226 | if (ed.getVersion() == 1) { |
---|
227 | profile = FCSTestProfile.CLARIN_FCS_1_0; |
---|
228 | } |
---|
229 | } else { |
---|
230 | logger.debug("assume legacy"); |
---|
231 | profile = FCSTestProfile.CLARIN_FCS_LEGACY; |
---|
232 | } |
---|
233 | |
---|
234 | if (profile == null) { |
---|
235 | request = new SRUExplainRequest(endpointURI); |
---|
236 | request.setStrictMode(false); |
---|
237 | request.setVersion(SRUVersion.VERSION_2_0); |
---|
238 | request.setExtraRequestData( |
---|
239 | ClarinFCSConstants.X_FCS_ENDPOINT_DESCRIPTION, |
---|
240 | ClarinFCSConstants.TRUE); |
---|
241 | request.setParseRecordDataEnabled(true); |
---|
242 | try { |
---|
243 | response = client.explain(request); |
---|
244 | |
---|
245 | ed = response.getFirstExtraResponseData( |
---|
246 | ClarinFCSEndpointDescription.class); |
---|
247 | if (ed != null) { |
---|
248 | if (ed.getVersion() == 2) { |
---|
249 | profile = FCSTestProfile.CLARIN_FCS_2_0; |
---|
250 | } |
---|
251 | } |
---|
252 | } catch (SRUClientException e) { |
---|
253 | if ((e.getMessage() != null) && (e.getMessage() |
---|
254 | .contains("responded with different version"))) { |
---|
255 | throw new SRUClientException( |
---|
256 | "Seriously broken Endpoint: when trying to " + |
---|
257 | "detect FCS 2.0 the Endpoint illegally " + |
---|
258 | "responded with a SRU 1.2 reponse to a " + |
---|
259 | "SRU 2.0 request!"); |
---|
260 | } else { |
---|
261 | throw e; |
---|
262 | } |
---|
263 | } |
---|
264 | } |
---|
265 | if (profile != null) { |
---|
266 | final FCSTestContext context = |
---|
267 | new FCSTestContext(profile, |
---|
268 | endpointURI, |
---|
269 | searchTerm, |
---|
270 | strictMode, |
---|
271 | connectTimeout, |
---|
272 | socketTimeout); |
---|
273 | return context; |
---|
274 | } |
---|
275 | } catch (SRUClientException e) { |
---|
276 | logger.error("error", e); |
---|
277 | throw new SRUClientException("An error occured while " + |
---|
278 | "auto-detecting CLARIN-FCS version", e); |
---|
279 | } |
---|
280 | throw new SRUClientException("Unable to auto-detect CLARIN-FCS version!"); |
---|
281 | } |
---|
282 | |
---|
283 | |
---|
284 | private void doPerformTests(final ProgressListener listener, |
---|
285 | final FCSTestContext context) { |
---|
286 | /* make sure that we'll capture the logging records */ |
---|
287 | java.util.logging.Logger l = |
---|
288 | java.util.logging.Logger.getLogger("eu.clarin.sru"); |
---|
289 | l.setLevel(Level.FINEST); |
---|
290 | boolean found = false; |
---|
291 | Handler[] handlers = l.getHandlers(); |
---|
292 | if (handlers != null) { |
---|
293 | for (Handler handler : handlers) { |
---|
294 | if (handler == logcapturehandler) { |
---|
295 | found = true; |
---|
296 | break; |
---|
297 | } |
---|
298 | } |
---|
299 | } |
---|
300 | if (!found) { |
---|
301 | l.addHandler(logcapturehandler); |
---|
302 | } |
---|
303 | |
---|
304 | |
---|
305 | List<FCSTest> tests = new ArrayList<FCSTest>(); |
---|
306 | for (FCSTest test : TESTS) { |
---|
307 | final FCSTestCase tc = |
---|
308 | test.getClass().getAnnotation(FCSTestCase.class); |
---|
309 | if (Arrays.binarySearch(tc.profiles(), context.getProfile()) >= 0) { |
---|
310 | tests.add(test); |
---|
311 | } |
---|
312 | } |
---|
313 | |
---|
314 | List<FCSTestResult> results = null; |
---|
315 | |
---|
316 | final SRUClient client = context.getClient(); |
---|
317 | final int totalCount = tests.size(); |
---|
318 | int num = 1; |
---|
319 | listener.updateMaximum(totalCount); |
---|
320 | for (FCSTest test : tests) { |
---|
321 | if (results == null) { |
---|
322 | results = new LinkedList<FCSTestResult>(); |
---|
323 | } |
---|
324 | logger.debug("running test {}:{}", num, test.getName()); |
---|
325 | final String message = String.format( |
---|
326 | "Performing test \"%s:%s\" (%d/%d) ...", |
---|
327 | context.getProfile().toDisplayString(), |
---|
328 | test.getName(), num, totalCount); |
---|
329 | listener.updateProgress(message); |
---|
330 | num++; |
---|
331 | |
---|
332 | FCSTestResult result = null; |
---|
333 | try { |
---|
334 | logcapturehandler.publish(new LogRecord(Level.FINE, |
---|
335 | "running test class " + test.getClass().getName())); |
---|
336 | result = test.perform(context, client); |
---|
337 | result.setLogRecords(logcapturehandler.getLogRecords()); |
---|
338 | } catch (SRUClientException e) { |
---|
339 | String msg = e.getMessage(); |
---|
340 | if (msg == null) { |
---|
341 | msg = "unclassified exception from SRUClient"; |
---|
342 | } |
---|
343 | final LogRecord record = new LogRecord(Level.SEVERE, msg); |
---|
344 | record.setThrown(e); |
---|
345 | logcapturehandler.publish(record); |
---|
346 | result = new FCSTestResult(test, |
---|
347 | FCSTestResult.Code.ERROR, |
---|
348 | msg, |
---|
349 | logcapturehandler.getLogRecords()); |
---|
350 | } catch (Throwable t) { |
---|
351 | final LogRecord record = new LogRecord(Level.SEVERE, |
---|
352 | "The endpoint tester as triggered an internal error! " + |
---|
353 | "Please report to developers."); |
---|
354 | logcapturehandler.publish(record); |
---|
355 | result = new FCSTestResult(test, |
---|
356 | FCSTestResult.Code.ERROR, |
---|
357 | record.getMessage(), |
---|
358 | logcapturehandler.getLogRecords()); |
---|
359 | logger.error("an internal error occured", t); |
---|
360 | } |
---|
361 | results.add(result); |
---|
362 | } // for |
---|
363 | listener.onDone(context, results); |
---|
364 | } |
---|
365 | |
---|
366 | |
---|
367 | private void doPerformProbeRequest(final ProgressListener listener, |
---|
368 | final String baseURI) throws IOException { |
---|
369 | try { |
---|
370 | logger.debug("performing initial probe request to {}", |
---|
371 | baseURI); |
---|
372 | listener.updateProgress( |
---|
373 | "Performing HTTP HEAD probe request ..."); |
---|
374 | final CloseableHttpClient client = |
---|
375 | createHttpClient(DEFAULT_CONNECT_TIMEOUT, |
---|
376 | DEFAULT_SOCKET_TIMEOUT); |
---|
377 | final HttpHead request = new HttpHead(baseURI); |
---|
378 | HttpResponse response = null; |
---|
379 | try { |
---|
380 | response = client.execute(request); |
---|
381 | StatusLine status = response.getStatusLine(); |
---|
382 | if (status.getStatusCode() != HttpStatus.SC_OK) { |
---|
383 | throw new IOException("Probe request to endpoint " + |
---|
384 | "returned unexpected HTTP status " + |
---|
385 | status.getStatusCode()); |
---|
386 | } |
---|
387 | } finally { |
---|
388 | HttpClientUtils.closeQuietly(response); |
---|
389 | |
---|
390 | HttpClientUtils.closeQuietly(client); |
---|
391 | } |
---|
392 | } catch (ClientProtocolException e) { |
---|
393 | throw new IOException(e); |
---|
394 | } |
---|
395 | } |
---|
396 | |
---|
397 | |
---|
398 | @Override |
---|
399 | public void contextInitialized(ServletContextEvent event) { |
---|
400 | logger.info("initialized"); |
---|
401 | } |
---|
402 | |
---|
403 | |
---|
404 | @Override |
---|
405 | public void contextDestroyed(ServletContextEvent event) { |
---|
406 | logger.info("shutting down ..."); |
---|
407 | INSTANCE.destroy(); |
---|
408 | } |
---|
409 | |
---|
410 | |
---|
411 | static { |
---|
412 | List<FCSTest> tests = null; |
---|
413 | try { |
---|
414 | Reflections reflections = new Reflections(TESTCASE_PACKAGE); |
---|
415 | Set<Class<?>> annotations = |
---|
416 | reflections.getTypesAnnotatedWith(FCSTestCase.class); |
---|
417 | if ((annotations != null) && !annotations.isEmpty()) { |
---|
418 | List<Class<?>> classes = new ArrayList<Class<?>>( |
---|
419 | annotations.size()); |
---|
420 | for (Class<?> clazz : annotations) { |
---|
421 | FCSTestCase tc = clazz.getAnnotation(FCSTestCase.class); |
---|
422 | if ((tc != null) && (tc.priority() >= 0)) { |
---|
423 | classes.add(clazz); |
---|
424 | } |
---|
425 | } |
---|
426 | Collections.sort(classes, new Comparator<Class<?>>() { |
---|
427 | @Override |
---|
428 | public int compare(Class<?> o1, Class<?> o2) { |
---|
429 | FCSTestCase a1 = o1.getAnnotation(FCSTestCase.class); |
---|
430 | FCSTestCase a2 = o2.getAnnotation(FCSTestCase.class); |
---|
431 | return a1.priority() - a2.priority(); |
---|
432 | } |
---|
433 | }); |
---|
434 | for (Class<?> clazz : classes) { |
---|
435 | if (tests == null) { |
---|
436 | tests = new ArrayList<FCSTest>(classes.size()); |
---|
437 | } |
---|
438 | tests.add((FCSTest) clazz.newInstance()); |
---|
439 | } |
---|
440 | } |
---|
441 | } catch (InstantiationException e) { |
---|
442 | e.printStackTrace(); |
---|
443 | } catch (IllegalAccessException e) { |
---|
444 | e.printStackTrace(); |
---|
445 | } |
---|
446 | |
---|
447 | if (tests != null) { |
---|
448 | TESTS = Collections.unmodifiableList(tests); |
---|
449 | } else { |
---|
450 | TESTS = Collections.emptyList(); |
---|
451 | } |
---|
452 | } |
---|
453 | |
---|
454 | } // class SRUEndpointTester |
---|