1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.commons.httpclient;
29
30 import java.io.IOException;
31 import java.lang.ref.WeakReference;
32 import java.net.InetAddress;
33 import java.net.Socket;
34 import java.net.UnknownHostException;
35
36 import junit.framework.Test;
37 import junit.framework.TestSuite;
38
39 import org.apache.commons.httpclient.methods.GetMethod;
40 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
41 import org.apache.commons.httpclient.params.HttpConnectionParams;
42 import org.apache.commons.httpclient.protocol.Protocol;
43 import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
44 import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
45 import org.apache.commons.httpclient.server.SimpleRequest;
46 import org.apache.commons.httpclient.server.SimpleResponse;
47
48 /***
49 * Unit tests for {@link HttpConnectionManager}.
50 *
51 * @author Marc A. Saegesser
52 * @version $Id: TestHttpConnectionManager.java 312494 2005-10-09 19:53:32Z olegk $
53 */
54 public class TestHttpConnectionManager extends HttpClientTestBase {
55
56
57 public TestHttpConnectionManager(String testName) throws IOException {
58 super(testName);
59 }
60
61
62 public static void main(String args[]) {
63 String[] testCaseName = { TestHttpConnectionManager.class.getName() };
64 junit.textui.TestRunner.main(testCaseName);
65 }
66
67
68
69 public static Test suite() {
70 return new TestSuite(TestHttpConnectionManager.class);
71 }
72
73
74
75
76 /***
77 * Test that the ConnectMethod correctly releases connections when
78 * CONNECT fails.
79 */
80 public void testConnectMethodFailureRelease() throws Exception {
81
82 MultiThreadedHttpConnectionManager mgr = new MultiThreadedHttpConnectionManager();
83 mgr.getParams().setIntParameter(
84 HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS, 1);
85 client.setHttpConnectionManager(mgr);
86 this.server.setHttpService(new RejectConnectService());
87
88
89
90
91 client.getHostConfiguration().setProxy(server.getLocalAddress(), server.getLocalPort());
92
93
94 client.getHostConfiguration().setHost(
95 "notARealHost",
96 1234,
97 new Protocol(
98 "https",
99 (ProtocolSocketFactory)new FakeSecureProtocolSocketFactory(),
100 443)
101 );
102
103 GetMethod get = new GetMethod("/");
104 try {
105 assertTrue(client.executeMethod(get) != 200);
106 } catch (IOException e) {
107 e.printStackTrace();
108 fail("Error executing connect: " + e);
109 }
110
111
112 try {
113 get.releaseConnection();
114 mgr.getConnectionWithTimeout(client.getHostConfiguration(), 1).releaseConnection();
115 } catch (ConnectTimeoutException e1) {
116 fail("Connection should have been available.");
117 }
118
119 get = new GetMethod("/");
120
121 try {
122 assertTrue(client.executeMethod(get) != 200);
123 } catch (IOException e) {
124 e.printStackTrace();
125 fail("Error executing connect: " + e);
126 }
127
128
129 try {
130 get.getResponseBodyAsString();
131 mgr.getConnectionWithTimeout(client.getHostConfiguration(), 1).releaseConnection();
132 } catch (ConnectTimeoutException e1) {
133 fail("Connection should have been available.");
134 }
135
136 get = new GetMethod("/");
137
138 try {
139 assertTrue(client.executeMethod(get) != 200);
140 } catch (IOException e) {
141 e.printStackTrace();
142 fail("Error executing connect: " + e);
143 }
144
145
146 try {
147 get.getResponseBodyAsStream().close();
148 mgr.getConnectionWithTimeout(client.getHostConfiguration(), 1).releaseConnection();
149 } catch (ConnectTimeoutException e) {
150 fail("Connection should have been available.");
151 } catch (IOException e) {
152 e.printStackTrace();
153 fail("Close connection failed: " + e);
154 }
155 }
156
157 public void testGetConnection() {
158 MultiThreadedHttpConnectionManager mgr = new MultiThreadedHttpConnectionManager();
159
160 HostConfiguration hostConfiguration = new HostConfiguration();
161 hostConfiguration.setHost("www.nosuchserver.com", 80, "http");
162
163
164 HttpConnection conn = mgr.getConnection(hostConfiguration);
165
166 assertEquals("Host", "www.nosuchserver.com", conn.getHost());
167 assertEquals("Port", 80, conn.getPort());
168
169 mgr.releaseConnection(conn);
170
171
172 hostConfiguration.setHost("www.nosuchserver.com", -1, "https");
173 conn = mgr.getConnection(hostConfiguration);
174
175 assertEquals("Host", "www.nosuchserver.com", conn.getHost());
176 assertEquals("Port", 443, conn.getPort());
177
178 mgr.releaseConnection(conn);
179
180
181 hostConfiguration.setHost("www.nowhere.org", 8080, "http");
182 conn = mgr.getConnection(hostConfiguration);
183
184 assertEquals("Host", "www.nowhere.org", conn.getHost());
185 assertEquals("Port", 8080, conn.getPort());
186
187 mgr.releaseConnection(conn);
188
189 }
190
191 public void testDroppedThread() throws Exception {
192
193 this.server.setHttpService(new EchoService());
194
195 MultiThreadedHttpConnectionManager mthcm = new MultiThreadedHttpConnectionManager();
196 client.setHttpConnectionManager(mthcm);
197 WeakReference wr = new WeakReference(mthcm);
198
199 GetMethod method = new GetMethod("/");
200 client.executeMethod(method);
201 method.releaseConnection();
202
203 mthcm = null;
204 client = null;
205 method = null;
206
207 System.gc();
208
209
210
211 try {
212 Thread.sleep(1000);
213 } catch (InterruptedException e) {
214 fail("shouldn't be interrupted.");
215 }
216
217 Object connectionManager = wr.get();
218 assertNull("connectionManager should be null", connectionManager);
219 }
220
221 public void testWriteRequestReleaseConnection() {
222
223 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
224 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
225
226 client.setHttpConnectionManager(connectionManager);
227
228 GetMethod get = new GetMethod("/") {
229 protected boolean writeRequestBody(HttpState state, HttpConnection conn)
230 throws IOException, HttpException {
231 throw new IOException("Oh no!!");
232 }
233 };
234
235 try {
236 client.executeMethod(get);
237 fail("An exception should have occurred.");
238 } catch (HttpException e) {
239 e.printStackTrace();
240 fail("HttpException should not have occurred: " + e);
241 } catch (IOException e) {
242
243 }
244
245 try {
246 connectionManager.getConnectionWithTimeout(client.getHostConfiguration(), 1);
247 } catch (ConnectTimeoutException e) {
248 e.printStackTrace();
249 fail("Connection was not released: " + e);
250 }
251
252 }
253
254 public void testReleaseConnection() {
255
256 this.server.setHttpService(new EchoService());
257
258 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
259 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
260
261 client.setHttpConnectionManager(connectionManager);
262
263 client.getParams().setConnectionManagerTimeout(1);
264
265 GetMethod getMethod = new GetMethod("/");
266
267 try {
268 client.executeMethod(getMethod);
269 } catch (Exception e) {
270 fail("error reading from server: " + e);
271 }
272
273 try {
274
275 client.executeMethod(getMethod);
276 fail("a httpConnection should not be available");
277 } catch (ConnectTimeoutException e) {
278 } catch (HttpException e) {
279 fail("error reading from server; " + e);
280 } catch (IOException e) {
281 e.printStackTrace();
282 fail("error reading from server; " + e);
283 }
284
285
286 getMethod.releaseConnection();
287
288 getMethod = new GetMethod("/");
289
290 try {
291
292 client.executeMethod(getMethod);
293 } catch (HttpException e) {
294 fail("httpConnection does not appear to have been released: " + e);
295 } catch (IOException e) {
296 fail("error reading from server; " + e);
297 }
298
299 }
300
301 /***
302 * Makes sure that a connection gets released after the content of the body
303 * is read.
304 */
305 public void testResponseAutoRelease() throws Exception {
306
307 this.server.setHttpService(new EchoService());
308
309 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
310 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
311
312 client.setHttpConnectionManager(connectionManager);
313
314 client.getParams().setConnectionManagerTimeout( 1 );
315
316 GetMethod getMethod = new GetMethod("/");
317
318 try {
319 client.executeMethod(getMethod);
320 } catch (Exception e) {
321 fail("error reading from server: " + e);
322 }
323
324
325 getMethod.getResponseBody();
326
327 getMethod = new GetMethod("/");
328
329 try {
330
331 client.executeMethod(getMethod);
332 } catch (HttpException e) {
333 fail("httpConnection does not appear to have been released: " + e);
334 } catch (IOException e) {
335 fail("error reading from server; " + e);
336 }
337
338 }
339
340 /***
341 * Tests the MultiThreadedHttpConnectionManager's ability to reclaim unused
342 * connections.
343 */
344 public void testConnectionReclaiming() {
345
346 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
347 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
348 connectionManager.getParams().setMaxTotalConnections(1);
349
350 HostConfiguration host1 = new HostConfiguration();
351 host1.setHost("host1", -1, "http");
352
353 HostConfiguration host2 = new HostConfiguration();
354 host2.setHost("host2", -1, "http");
355
356 HttpConnection connection = connectionManager.getConnection(host1);
357
358 connection.releaseConnection();
359 connection = null;
360
361 try {
362
363 connection = connectionManager.getConnectionWithTimeout(host2, 100);
364 } catch (ConnectTimeoutException e) {
365 e.printStackTrace();
366 fail("a httpConnection should have been available: " + e);
367 }
368 }
369
370 /***
371 * Tests that {@link MultiThreadedHttpConnectionManager#shutdownAll()} closes all resources
372 * and makes all connection mangers unusable.
373 */
374 public void testShutdownAll() {
375
376 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
377 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
378 connectionManager.getParams().setMaxTotalConnections(1);
379
380 HostConfiguration host1 = new HostConfiguration();
381 host1.setHost("host1", -1, "http");
382
383
384 HttpConnection connection = connectionManager.getConnection(host1);
385
386
387 GetConnectionThread getConn = new GetConnectionThread(host1, connectionManager, 0);
388 getConn.start();
389
390 MultiThreadedHttpConnectionManager.shutdownAll();
391
392
393 connection.releaseConnection();
394 connection = null;
395
396 try {
397 getConn.join();
398 } catch (InterruptedException e) {
399 e.printStackTrace();
400 }
401
402
403 assertNull("Not connection should have been checked out", getConn.getConnection());
404 assertNotNull("There should have been an exception", getConn.getException());
405
406 try {
407 connectionManager.getConnection(host1);
408 fail("An exception should have occurred");
409 } catch (Exception e) {
410
411 }
412 }
413
414 /***
415 * Tests that {@link MultiThreadedHttpConnectionManager#shutdown()} closes all resources
416 * and makes the connection manger unusable.
417 */
418 public void testShutdown() {
419
420 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
421 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
422 connectionManager.getParams().setMaxTotalConnections(1);
423
424 HostConfiguration host1 = new HostConfiguration();
425 host1.setHost("host1", -1, "http");
426
427
428 HttpConnection connection = connectionManager.getConnection(host1);
429
430
431 GetConnectionThread getConn = new GetConnectionThread(host1, connectionManager, 0);
432 getConn.start();
433
434 connectionManager.shutdown();
435
436
437 connection.releaseConnection();
438 connection = null;
439
440 try {
441 getConn.join();
442 } catch (InterruptedException e) {
443 e.printStackTrace();
444 }
445
446
447 assertNull("Not connection should have been checked out", getConn.getConnection());
448 assertNotNull("There should have been an exception", getConn.getException());
449
450 try {
451 connectionManager.getConnection(host1);
452 fail("An exception should have occurred");
453 } catch (Exception e) {
454
455 }
456 }
457
458 /***
459 * Tests the MultiThreadedHttpConnectionManager's ability to restrict the maximum number
460 * of connections.
461 */
462 public void testMaxConnections() {
463
464 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
465 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
466 connectionManager.getParams().setMaxTotalConnections(2);
467
468 HostConfiguration host1 = new HostConfiguration();
469 host1.setHost("host1", -1, "http");
470
471 HostConfiguration host2 = new HostConfiguration();
472 host2.setHost("host2", -1, "http");
473
474 HttpConnection connection1 = connectionManager.getConnection(host1);
475 HttpConnection connection2 = connectionManager.getConnection(host2);
476
477 try {
478
479 connectionManager.getConnectionWithTimeout(host2, 100);
480 fail("ConnectionPoolTimeoutException should not be available");
481 } catch (ConnectionPoolTimeoutException e) {
482
483 }
484
485
486 connection2.releaseConnection();
487 connection2 = null;
488
489 try {
490
491 connection2 = connectionManager.getConnectionWithTimeout(host2, 100);
492 } catch (ConnectionPoolTimeoutException e) {
493 e.printStackTrace();
494 fail("a httpConnection should have been available: " + e);
495 }
496 }
497
498 /***
499 * Tests the MultiThreadedHttpConnectionManager's ability to restrict the maximum number
500 * of connections per host.
501 */
502 public void testMaxConnectionsPerHost() throws Exception {
503
504 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
505 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
506 connectionManager.getParams().setMaxTotalConnections(100);
507
508 HostConfiguration host1 = new HostConfiguration();
509 host1.setHost("host1", -1, "http");
510
511 HostConfiguration host2 = new HostConfiguration();
512 host2.setHost("host2", -1, "http");
513
514 HostConfiguration host3 = new HostConfiguration();
515 host3.setHost("host3", -1, "http");
516
517 connectionManager.getParams().setMaxConnectionsPerHost(host1, 3);
518 connectionManager.getParams().setMaxConnectionsPerHost(host2, 2);
519
520
521 HttpConnection connection1 = connectionManager.getConnectionWithTimeout(host1, 1000);
522 HttpConnection connection2 = connectionManager.getConnectionWithTimeout(host1, 1000);
523 HttpConnection connection3 = connectionManager.getConnectionWithTimeout(host1, 1000);
524 try {
525
526 connectionManager.getConnectionWithTimeout(host1, 100);
527 fail("ConnectionPoolTimeoutException should not be available");
528 } catch (ConnectionPoolTimeoutException e) {
529
530 }
531
532
533 connection1 = connectionManager.getConnectionWithTimeout(host2, 1000);
534 connection2 = connectionManager.getConnectionWithTimeout(host2, 1000);
535 try {
536
537 connectionManager.getConnectionWithTimeout(host2, 100);
538 fail("ConnectionPoolTimeoutException should not be available");
539 } catch (ConnectionPoolTimeoutException e) {
540
541 }
542
543
544 connection1 = connectionManager.getConnectionWithTimeout(host3, 1000);
545 try {
546
547 connectionManager.getConnectionWithTimeout(host3, 100);
548 fail("ConnectionPoolTimeoutException should not be available");
549 } catch (ConnectionPoolTimeoutException e) {
550
551 }
552 }
553
554 public void testHostReusePreference() {
555
556 final MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
557 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
558 connectionManager.getParams().setMaxTotalConnections(1);
559
560 final HostConfiguration host1 = new HostConfiguration();
561 host1.setHost("host1", -1, "http");
562
563 final HostConfiguration host2 = new HostConfiguration();
564 host2.setHost("host2", -1, "http");
565
566 HttpConnection connection = connectionManager.getConnection(host1);
567
568 GetConnectionThread getHost1 = new GetConnectionThread(host1, connectionManager, 200);
569 GetConnectionThread getHost2 = new GetConnectionThread(host2, connectionManager, 200);
570
571 getHost2.start();
572 getHost1.start();
573
574
575 try {
576 Thread.sleep(100);
577 } catch (InterruptedException e1) {
578 e1.printStackTrace();
579 }
580
581
582 connection.releaseConnection();
583 connection = null;
584
585 try {
586 getHost1.join();
587 getHost2.join();
588 } catch (InterruptedException e) {
589 e.printStackTrace();
590 }
591
592 assertNotSame(
593 "Connection should have been given to someone",
594 getHost1.getConnection(),
595 getHost2.getConnection()
596 );
597 assertNotNull("Connection should have been given to host1", getHost1.getConnection());
598 assertNull("Connection should NOT have been given to host2", getHost2.getConnection());
599
600 }
601
602 public void testMaxConnectionsPerServer() {
603
604 this.server.setHttpService(new EchoService());
605
606 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
607 connectionManager.getParams().setDefaultMaxConnectionsPerHost(1);
608
609 client.setHttpConnectionManager(connectionManager);
610
611 client.getParams().setConnectionManagerTimeout( 1 );
612
613 GetMethod getMethod = new GetMethod("/");
614
615 try {
616 client.executeMethod(getMethod);
617 } catch (Exception e) {
618 fail("error reading from server: " + e);
619 }
620
621 GetMethod getMethod2 = new GetMethod("/");
622
623 try {
624
625 client.executeMethod(getMethod2);
626 fail("a httpConnection should not be available");
627 } catch (ConnectTimeoutException e) {
628 } catch (HttpException e) {
629 fail("error reading from server; " + e);
630 } catch (IOException e) {
631 fail("error reading from server; " + e);
632 }
633
634 }
635
636 public void testDeleteClosedConnections() {
637
638 MultiThreadedHttpConnectionManager manager = new MultiThreadedHttpConnectionManager();
639
640 HttpConnection conn = manager.getConnection(client.getHostConfiguration());
641
642 assertEquals("connectionsInPool", manager.getConnectionsInPool(), 1);
643 assertEquals("connectionsInPool(host)", manager.getConnectionsInPool(client.getHostConfiguration()), 1);
644
645 conn.close();
646 conn.releaseConnection();
647
648 assertEquals("connectionsInPool", manager.getConnectionsInPool(), 1);
649 assertEquals("connectionsInPool(host)", manager.getConnectionsInPool(client.getHostConfiguration()), 1);
650
651 manager.deleteClosedConnections();
652
653 assertEquals("connectionsInPool", manager.getConnectionsInPool(), 0);
654 assertEquals("connectionsInPool(host)", manager.getConnectionsInPool(client.getHostConfiguration()), 0);
655 }
656
657 public void testReclaimUnusedConnection() {
658
659 this.server.setHttpService(new EchoService());
660
661 MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
662 connectionManager.getParams().setIntParameter(
663 HttpConnectionManagerParams.MAX_TOTAL_CONNECTIONS, 1);
664
665 client.setHttpConnectionManager(connectionManager);
666
667 client.getParams().setConnectionManagerTimeout( 30000 );
668
669 GetMethod getMethod = new GetMethod("/");
670
671 try {
672 client.executeMethod(getMethod);
673 } catch (Exception e) {
674 fail("error reading from server: " + e);
675 }
676
677 getMethod = new GetMethod("/");
678
679 Runtime.getRuntime().gc();
680
681 try {
682
683
684 client.executeMethod(getMethod);
685 } catch (HttpException e) {
686 fail("httpConnection does not appear to have been reclaimed by the GC: " + e);
687 } catch (IOException e) {
688 fail("error reading from server; " + e);
689 }
690
691 }
692
693 public void testGetFromMultipleThreads() {
694
695 this.server.setHttpService(new EchoService());
696
697 client.setHttpConnectionManager(new MultiThreadedHttpConnectionManager());
698 ExecuteMethodThread[] threads = new ExecuteMethodThread[10];
699
700 for (int i = 0; i < threads.length; i++) {
701 GetMethod method = new GetMethod("/");
702 method.setFollowRedirects(true);
703
704 threads[i] = new ExecuteMethodThread(method, client);
705 threads[i].start();
706 }
707
708 for (int i = 0; i < threads.length; i++) {
709 try {
710
711
712 threads[i].join(10000);
713 } catch (InterruptedException e) {
714 }
715
716 Exception e = threads[i].getException();
717 if (e != null) {
718 fail("An error occured in the get: " + e);
719 }
720
721 assertEquals(threads[i].getMethod().getStatusCode(), HttpStatus.SC_OK);
722 }
723 }
724
725 public void testTimeout() {
726 MultiThreadedHttpConnectionManager mgr = new MultiThreadedHttpConnectionManager();
727 mgr.getParams().setDefaultMaxConnectionsPerHost(2);
728
729 try{
730 HostConfiguration hostConfig = new HostConfiguration();
731 hostConfig.setHost("www.nosuchserver.com", 80, "http");
732
733 HttpConnection conn1 = mgr.getConnection(hostConfig);
734 HttpConnection conn2 = mgr.getConnection(hostConfig);
735
736 HttpConnection conn3 = mgr.getConnectionWithTimeout(hostConfig, 1000);
737 fail("Expected an HttpException.");
738
739 }catch(ConnectTimeoutException e){
740
741 }
742 }
743
744 static class FakeSecureProtocolSocketFactory implements SecureProtocolSocketFactory {
745
746 public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
747 throws IOException, UnknownHostException {
748 throw new IllegalStateException("createSocket() should never have been called.");
749 }
750
751 public Socket createSocket(String host, int port)
752 throws IOException, UnknownHostException {
753 throw new IllegalStateException("createSocket() should never have been called.");
754 }
755
756 public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort)
757 throws IOException, UnknownHostException {
758 throw new IllegalStateException("createSocket() should never have been called.");
759 }
760
761 public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort,
762 HttpConnectionParams params)
763 throws IOException, UnknownHostException {
764 throw new IllegalStateException("createSocket() should never have been called.");
765 }
766 }
767
768 static class RejectConnectService extends EchoService {
769 public boolean process(SimpleRequest request, SimpleResponse response)
770 throws IOException {
771 if (request.getRequestLine().getMethod().equalsIgnoreCase("CONNECT")) {
772 response.setStatusLine(request.getRequestLine().getHttpVersion(), HttpStatus.SC_METHOD_NOT_ALLOWED);
773 response.setHeader(new Header("Connection", "close"));
774 return true;
775 } else {
776 return super.process(request, response);
777 }
778 }
779 }
780
781 static class GetConnectionThread extends Thread {
782
783 private HostConfiguration hostConfiguration;
784 private MultiThreadedHttpConnectionManager connectionManager;
785 private HttpConnection connection;
786 private long timeout;
787 private Exception exception;
788
789 public GetConnectionThread(
790 HostConfiguration hostConfiguration,
791 MultiThreadedHttpConnectionManager connectionManager,
792 long timeout
793 ) {
794 this.hostConfiguration = hostConfiguration;
795 this.connectionManager = connectionManager;
796 this.timeout = timeout;
797 }
798
799 public void run() {
800 try {
801 connection = connectionManager.getConnectionWithTimeout(hostConfiguration, timeout);
802 } catch (Exception e) {
803 this.exception = e;
804 }
805 }
806
807 public Exception getException() {
808 return exception;
809 }
810
811 public HttpConnection getConnection() {
812 return connection;
813 }
814
815 }
816
817 }
818