-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
RestfulServer.java
1679 lines (1487 loc) 路 59.9 KB
/
RestfulServer.java
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package ca.uhn.fhir.rest.server;
/*
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import ca.uhn.fhir.context.ConfigurationException;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.ProvidedResourceScanner;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.api.AddProfileTagEnum;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.annotation.Destroy;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.server.IFhirVersionServer;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.ParseAction;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.tenant.ITenantIdentificationStrategy;
import ca.uhn.fhir.util.*;
import com.google.common.collect.Lists;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.Manifest;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("WeakerAccess")
public class RestfulServer extends HttpServlet implements IRestfulServer<ServletRequestDetails> {
/**
* All incoming requests will have an attribute added to {@link HttpServletRequest#getAttribute(String)}
* with this key. The value will be a Java {@link Date} with the time that request processing began.
*/
public static final String REQUEST_START_TIME = RestfulServer.class.getName() + "REQUEST_START_TIME";
/**
* Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED}
*/
public static final ETagSupportEnum DEFAULT_ETAG_SUPPORT = ETagSupportEnum.ENABLED;
/**
* Requests will have an HttpServletRequest attribute set with this name, containing the servlet
* context, in order to avoid a dependency on Servlet-API 3.0+
*/
public static final String SERVLET_CONTEXT_ATTRIBUTE = "ca.uhn.fhir.rest.server.RestfulServer.servlet_context";
private static final ExceptionHandlingInterceptor DEFAULT_EXCEPTION_HANDLER = new ExceptionHandlingInterceptor();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class);
private static final long serialVersionUID = 1L;
private final List<IServerInterceptor> myInterceptors = new ArrayList<>();
private final List<Object> myPlainProviders = new ArrayList<>();
private final List<IResourceProvider> myResourceProviders = new ArrayList<>();
private BundleInclusionRule myBundleInclusionRule = BundleInclusionRule.BASED_ON_INCLUDES;
private boolean myDefaultPrettyPrint = false;
private EncodingEnum myDefaultResponseEncoding = EncodingEnum.XML;
private ETagSupportEnum myETagSupport = DEFAULT_ETAG_SUPPORT;
private FhirContext myFhirContext;
private boolean myIgnoreServerParsedRequestParameters = true;
private String myImplementationDescription;
private IPagingProvider myPagingProvider;
private Lock myProviderRegistrationMutex = new ReentrantLock();
private Map<String, ResourceBinding> myResourceNameToBinding = new HashMap<>();
private IServerAddressStrategy myServerAddressStrategy = new IncomingRequestAddressStrategy();
private ResourceBinding myServerBinding = new ResourceBinding();
private ResourceBinding myGlobalBinding = new ResourceBinding();
private BaseMethodBinding<?> myServerConformanceMethod;
private Object myServerConformanceProvider;
private String myServerName = "HAPI FHIR Server";
/**
* This is configurable but by default we just use HAPI version
*/
private String myServerVersion = createPoweredByHeaderProductVersion();
private boolean myStarted;
private boolean myUncompressIncomingContents = true;
private boolean myUseBrowserFriendlyContentTypes;
private ITenantIdentificationStrategy myTenantIdentificationStrategy;
private Date myConformanceDate;
/**
* Constructor. Note that if no {@link FhirContext} is passed in to the server (either through the constructor, or
* through {@link #setFhirContext(FhirContext)}) the server will determine which
* version of FHIR to support through classpath scanning. This is brittle, and it is highly recommended to explicitly
* specify a FHIR version.
*/
public RestfulServer() {
this(null);
}
/**
* Constructor
*/
public RestfulServer(FhirContext theCtx) {
myFhirContext = theCtx;
}
private void addContentLocationHeaders(RequestDetails theRequest, HttpServletResponse servletResponse, MethodOutcome response, String resourceName) {
if (response != null && response.getId() != null) {
addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_LOCATION, resourceName);
addLocationHeader(theRequest, servletResponse, response, Constants.HEADER_CONTENT_LOCATION, resourceName);
}
}
/**
* This method is called prior to sending a response to incoming requests. It is used to add custom headers.
* <p>
* Use caution if overriding this method: it is recommended to call <code>super.addHeadersToResponse</code> to avoid
* inadvertently disabling functionality.
* </p>
*/
public void addHeadersToResponse(HttpServletResponse theHttpResponse) {
String poweredByHeader = createPoweredByHeader();
if (isNotBlank(poweredByHeader)) {
theHttpResponse.addHeader(Constants.POWERED_BY_HEADER, poweredByHeader);
}
}
private void addLocationHeader(RequestDetails theRequest, HttpServletResponse theResponse, MethodOutcome response, String headerLocation, String resourceName) {
StringBuilder b = new StringBuilder();
b.append(theRequest.getFhirServerBase());
b.append('/');
b.append(resourceName);
b.append('/');
b.append(response.getId().getIdPart());
if (response.getId().hasVersionIdPart()) {
b.append("/" + Constants.PARAM_HISTORY + "/");
b.append(response.getId().getVersionIdPart());
}
theResponse.addHeader(headerLocation, b.toString());
}
private void assertProviderIsValid(Object theNext) throws ConfigurationException {
if (Modifier.isPublic(theNext.getClass().getModifiers()) == false) {
throw new ConfigurationException("Can not use provider '" + theNext.getClass() + "' - Class must be public");
}
}
public RestulfulServerConfiguration createConfiguration() {
RestulfulServerConfiguration result = new RestulfulServerConfiguration();
result.setResourceBindings(getResourceBindings());
result.setServerBindings(getServerBindings());
result.setImplementationDescription(getImplementationDescription());
result.setServerVersion(getServerVersion());
result.setServerName(getServerName());
result.setFhirContext(getFhirContext());
result.setServerAddressStrategy(myServerAddressStrategy);
try (InputStream inputStream = getClass().getResourceAsStream("/META-INF/MANIFEST.MF")) {
if (inputStream != null) {
Manifest manifest = new Manifest(inputStream);
String value = manifest.getMainAttributes().getValue("Build-Time");
result.setConformanceDate(new InstantDt(value));
}
} catch (Exception e) {
// fall through
}
return result;
}
protected List<String> createPoweredByAttributes() {
return Lists.newArrayList("FHIR Server", "FHIR " + myFhirContext.getVersion().getVersion().getFhirVersionString() + "/" + myFhirContext.getVersion().getVersion().name());
}
/**
* Subclasses may override to provide their own powered by
* header. Note that if you want to be nice and still credit HAPI
* FHIR you could consider overriding
* {@link #createPoweredByAttributes()} instead and adding your own
* fragments to the list.
*/
protected String createPoweredByHeader() {
StringBuilder b = new StringBuilder();
b.append(createPoweredByHeaderProductName());
b.append(" ");
b.append(createPoweredByHeaderProductVersion());
b.append(" ");
b.append(createPoweredByHeaderComponentName());
b.append(" (");
List<String> poweredByAttributes = createPoweredByAttributes();
for (ListIterator<String> iter = poweredByAttributes.listIterator(); iter.hasNext(); ) {
if (iter.nextIndex() > 0) {
b.append("; ");
}
b.append(iter.next());
}
b.append(")");
return b.toString();
}
/**
* Subclasses my override
*
* @see #createPoweredByHeader()
*/
protected String createPoweredByHeaderComponentName() {
return "REST Server";
}
/**
* Subclasses my override
*
* @see #createPoweredByHeader()
*/
protected String createPoweredByHeaderProductName() {
return "HAPI FHIR";
}
/**
* Subclasses my override
*
* @see #createPoweredByHeader()
*/
protected String createPoweredByHeaderProductVersion() {
return VersionUtil.getVersion();
}
@Override
public void destroy() {
if (getResourceProviders() != null) {
for (IResourceProvider iResourceProvider : getResourceProviders()) {
invokeDestroy(iResourceProvider);
}
}
if (myServerConformanceProvider != null) {
invokeDestroy(myServerConformanceProvider);
}
if (getPlainProviders() != null) {
for (Object next : getPlainProviders()) {
invokeDestroy(next);
}
}
}
/**
* Figure out and return whichever method binding is appropriate for
* the given request
*/
public BaseMethodBinding<?> determineResourceMethod(RequestDetails requestDetails, String requestPath) {
RequestTypeEnum requestType = requestDetails.getRequestType();
ResourceBinding resourceBinding = null;
BaseMethodBinding<?> resourceMethod = null;
String resourceName = requestDetails.getResourceName();
if (myServerConformanceMethod.incomingServerRequestMatchesMethod(requestDetails)) {
resourceMethod = myServerConformanceMethod;
} else if (resourceName == null) {
resourceBinding = myServerBinding;
} else {
resourceBinding = myResourceNameToBinding.get(resourceName);
if (resourceBinding == null) {
throwUnknownResourceTypeException(resourceName);
}
}
if (resourceMethod == null) {
if (resourceBinding != null) {
resourceMethod = resourceBinding.getMethod(requestDetails);
}
if (resourceMethod == null) {
resourceMethod = myGlobalBinding.getMethod(requestDetails);
}
}
if (resourceMethod == null) {
if (isBlank(requestPath)) {
throw new InvalidRequestException(myFhirContext.getLocalizer().getMessage(RestfulServer.class, "rootRequest"));
}
throwUnknownFhirOperationException(requestDetails, requestPath, requestType);
}
return resourceMethod;
}
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
handleRequest(RequestTypeEnum.DELETE, request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
handleRequest(RequestTypeEnum.GET, request, response);
}
@Override
protected void doOptions(HttpServletRequest theReq, HttpServletResponse theResp) throws ServletException, IOException {
handleRequest(RequestTypeEnum.OPTIONS, theReq, theResp);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
handleRequest(RequestTypeEnum.POST, request, response);
}
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
handleRequest(RequestTypeEnum.PUT, request, response);
}
/**
* Count length of URL string, but treating unescaped sequences (e.g. ' ') as their unescaped equivalent (%20)
*/
protected static int escapedLength(String theServletPath) {
int delta = 0;
for (int i = 0; i < theServletPath.length(); i++) {
char next = theServletPath.charAt(i);
if (next == ' ') {
delta = delta + 2;
}
}
return theServletPath.length() + delta;
}
private void findResourceMethods(Object theProvider) {
ourLog.info("Scanning type for RESTful methods: {}", theProvider.getClass());
int count = 0;
Class<?> clazz = theProvider.getClass();
Class<?> supertype = clazz.getSuperclass();
while (!Object.class.equals(supertype)) {
count += findResourceMethods(theProvider, supertype);
supertype = supertype.getSuperclass();
}
try {
count += findResourceMethods(theProvider, clazz);
} catch (ConfigurationException e) {
throw new ConfigurationException("Failure scanning class " + clazz.getSimpleName() + ": " + e.getMessage(), e);
}
if (count == 0) {
throw new ConfigurationException("Did not find any annotated RESTful methods on provider class " + theProvider.getClass().getCanonicalName());
}
}
private int findResourceMethods(Object theProvider, Class<?> clazz) throws ConfigurationException {
int count = 0;
for (Method m : ReflectionUtil.getDeclaredMethods(clazz)) {
BaseMethodBinding<?> foundMethodBinding = BaseMethodBinding.bindMethod(m, getFhirContext(), theProvider);
if (foundMethodBinding == null) {
continue;
}
count++;
if (foundMethodBinding instanceof ConformanceMethodBinding) {
myServerConformanceMethod = foundMethodBinding;
continue;
}
if (!Modifier.isPublic(m.getModifiers())) {
throw new ConfigurationException("Method '" + m.getName() + "' is not public, FHIR RESTful methods must be public");
}
if (Modifier.isStatic(m.getModifiers())) {
throw new ConfigurationException("Method '" + m.getName() + "' is static, FHIR RESTful methods must not be static");
}
ourLog.debug("Scanning public method: {}#{}", theProvider.getClass(), m.getName());
String resourceName = foundMethodBinding.getResourceName();
ResourceBinding resourceBinding;
if (resourceName == null) {
if (foundMethodBinding.isGlobalMethod()) {
resourceBinding = myGlobalBinding;
} else {
resourceBinding = myServerBinding;
}
} else {
RuntimeResourceDefinition definition = getFhirContext().getResourceDefinition(resourceName);
if (myResourceNameToBinding.containsKey(definition.getName())) {
resourceBinding = myResourceNameToBinding.get(definition.getName());
} else {
resourceBinding = new ResourceBinding();
resourceBinding.setResourceName(resourceName);
myResourceNameToBinding.put(resourceName, resourceBinding);
}
}
List<Class<?>> allowableParams = foundMethodBinding.getAllowableParamAnnotations();
if (allowableParams != null) {
for (Annotation[] nextParamAnnotations : m.getParameterAnnotations()) {
for (Annotation annotation : nextParamAnnotations) {
Package pack = annotation.annotationType().getPackage();
if (pack.equals(IdParam.class.getPackage())) {
if (!allowableParams.contains(annotation.annotationType())) {
throw new ConfigurationException("Method[" + m.toString() + "] is not allowed to have a parameter annotated with " + annotation);
}
}
}
}
}
resourceBinding.addMethod(foundMethodBinding);
ourLog.debug(" * Method: {}#{} is a handler", theProvider.getClass(), m.getName());
}
return count;
}
/**
* @deprecated As of HAPI FHIR 1.5, this property has been moved to
* {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)}
*/
@Override
@Deprecated
public AddProfileTagEnum getAddProfileTag() {
return myFhirContext.getAddProfileTagWhenEncoding();
}
/**
* Sets the profile tagging behaviour for the server. When set to a value other than {@link AddProfileTagEnum#NEVER}
* (which is the default), the server will automatically add a profile tag based on
* the class of the resource(s) being returned.
*
* @param theAddProfileTag The behaviour enum (must not be null)
* @deprecated As of HAPI FHIR 1.5, this property has been moved to
* {@link FhirContext#setAddProfileTagWhenEncoding(AddProfileTagEnum)}
*/
@Deprecated
@CoverageIgnore
public void setAddProfileTag(AddProfileTagEnum theAddProfileTag) {
Validate.notNull(theAddProfileTag, "theAddProfileTag must not be null");
myFhirContext.setAddProfileTagWhenEncoding(theAddProfileTag);
}
@Override
public BundleInclusionRule getBundleInclusionRule() {
return myBundleInclusionRule;
}
/**
* Set how bundle factory should decide whether referenced resources should be included in bundles
*
* @param theBundleInclusionRule - inclusion rule (@see BundleInclusionRule for behaviors)
*/
public void setBundleInclusionRule(BundleInclusionRule theBundleInclusionRule) {
myBundleInclusionRule = theBundleInclusionRule;
}
/**
* Returns the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either
* with the <code>_format</code> URL parameter, or with an <code>Accept</code> header
* in the request. The default is {@link EncodingEnum#XML}. Will not return null.
*/
@Override
public EncodingEnum getDefaultResponseEncoding() {
return myDefaultResponseEncoding;
}
/**
* Sets the default encoding to return (XML/JSON) if an incoming request does not specify a preference (either with
* the <code>_format</code> URL parameter, or with an <code>Accept</code> header in
* the request. The default is {@link EncodingEnum#XML}.
* <p>
* Note when testing this feature: Some browsers will include "application/xml" in their Accept header, which means
* that the
* </p>
*/
public void setDefaultResponseEncoding(EncodingEnum theDefaultResponseEncoding) {
Validate.notNull(theDefaultResponseEncoding, "theDefaultResponseEncoding can not be null");
myDefaultResponseEncoding = theDefaultResponseEncoding;
}
@Override
public ETagSupportEnum getETagSupport() {
return myETagSupport;
}
/**
* Sets (enables/disables) the server support for ETags. Must not be <code>null</code>. Default is
* {@link #DEFAULT_ETAG_SUPPORT}
*
* @param theETagSupport The ETag support mode
*/
public void setETagSupport(ETagSupportEnum theETagSupport) {
if (theETagSupport == null) {
throw new NullPointerException("theETagSupport can not be null");
}
myETagSupport = theETagSupport;
}
/**
* Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain
* providers should generally use this context if one is needed, as opposed to
* creating their own.
*/
@Override
public FhirContext getFhirContext() {
if (myFhirContext == null) {
//TODO: Use of a deprecated method should be resolved.
myFhirContext = new FhirContext();
}
return myFhirContext;
}
public void setFhirContext(FhirContext theFhirContext) {
Validate.notNull(theFhirContext, "FhirContext must not be null");
myFhirContext = theFhirContext;
}
public String getImplementationDescription() {
return myImplementationDescription;
}
public void setImplementationDescription(String theImplementationDescription) {
myImplementationDescription = theImplementationDescription;
}
/**
* Returns a list of all registered server interceptors
*/
@Override
public List<IServerInterceptor> getInterceptors() {
return Collections.unmodifiableList(myInterceptors);
}
/**
* Sets (or clears) the list of interceptors
*
* @param theInterceptors The list of interceptors (may be null)
*/
public void setInterceptors(IServerInterceptor... theInterceptors) {
Validate.noNullElements(theInterceptors, "theInterceptors must not contain any null elements");
myInterceptors.clear();
if (theInterceptors != null) {
myInterceptors.addAll(Arrays.asList(theInterceptors));
}
}
/**
* Sets (or clears) the list of interceptors
*
* @param theList The list of interceptors (may be null)
*/
public void setInterceptors(List<IServerInterceptor> theList) {
myInterceptors.clear();
if (theList != null) {
myInterceptors.addAll(theList);
}
}
@Override
public IPagingProvider getPagingProvider() {
return myPagingProvider;
}
/**
* Sets the paging provider to use, or <code>null</code> to use no paging (which is the default)
*/
public void setPagingProvider(IPagingProvider thePagingProvider) {
myPagingProvider = thePagingProvider;
}
/**
* Provides the non-resource specific providers which implement method calls on this server
*
* @see #getResourceProviders()
*/
public Collection<Object> getPlainProviders() {
return myPlainProviders;
}
/**
* Sets the non-resource specific providers which implement method calls on this server.
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Collection<Object> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myPlainProviders.clear();
if (theProviders != null) {
myPlainProviders.addAll(theProviders);
}
}
/**
* Sets the non-resource specific providers which implement method calls on this server.
*
* @see #setResourceProviders(Collection)
*/
public void setPlainProviders(Object... theProv) {
setPlainProviders(Arrays.asList(theProv));
}
/**
* Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path
* implementation
*
* @param requestFullPath the full request path
* @param servletContextPath the servelet context path
* @param servletPath the servelet path
* @return created resource path
*/
protected static String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {
return requestFullPath.substring(escapedLength(servletContextPath) + escapedLength(servletPath));
}
public Collection<ResourceBinding> getResourceBindings() {
return myResourceNameToBinding.values();
}
/**
* Provides the resource providers for this server
*/
public Collection<IResourceProvider> getResourceProviders() {
return myResourceProviders;
}
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(Collection<IResourceProvider> theProviders) {
Validate.noNullElements(theProviders, "theProviders must not contain any null elements");
myResourceProviders.clear();
if (theProviders != null) {
myResourceProviders.addAll(theProviders);
}
}
/**
* Sets the resource providers for this server
*/
public void setResourceProviders(IResourceProvider... theResourceProviders) {
myResourceProviders.clear();
if (theResourceProviders != null) {
myResourceProviders.addAll(Arrays.asList(theResourceProviders));
}
}
/**
* Get the server address strategy, which is used to determine what base URL to provide clients to refer to this
* server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
*/
public IServerAddressStrategy getServerAddressStrategy() {
return myServerAddressStrategy;
}
/**
* Provide a server address strategy, which is used to determine what base URL to provide clients to refer to this
* server. Defaults to an instance of {@link IncomingRequestAddressStrategy}
*/
public void setServerAddressStrategy(IServerAddressStrategy theServerAddressStrategy) {
Validate.notNull(theServerAddressStrategy, "Server address strategy can not be null");
myServerAddressStrategy = theServerAddressStrategy;
}
/**
* Returns the server base URL (with no trailing '/') for a given request
*/
public String getServerBaseForRequest(ServletRequestDetails theRequest) {
String fhirServerBase;
fhirServerBase = myServerAddressStrategy.determineServerBase(getServletContext(), theRequest.getServletRequest());
if (fhirServerBase.endsWith("/")) {
fhirServerBase = fhirServerBase.substring(0, fhirServerBase.length() - 1);
}
if (myTenantIdentificationStrategy != null) {
fhirServerBase = myTenantIdentificationStrategy.massageServerBaseUrl(fhirServerBase, theRequest);
}
return fhirServerBase;
}
/**
* Returns the method bindings for this server which are not specific to any particular resource type. This method is
* internal to HAPI and developers generally do not need to interact with it. Use
* with caution, as it may change.
*/
public List<BaseMethodBinding<?>> getServerBindings() {
return myServerBinding.getMethodBindings();
}
/**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement if one has been explicitly defined.
* <p>
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or
* set to <code>null</code> to use the appropriate one for the given FHIR version.
* </p>
*/
public Object getServerConformanceProvider() {
return myServerConformanceProvider;
}
/**
* Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement.
* <p>
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be
* changed, or set to <code>null</code> if you do not wish to export a conformance
* statement.
* </p>
* Note that this method can only be called before the server is initialized.
*
* @throws IllegalStateException Note that this method can only be called prior to {@link #init() initialization} and will throw an
* {@link IllegalStateException} if called after that.
*/
public void setServerConformanceProvider(Object theServerConformanceProvider) {
if (myStarted) {
throw new IllegalStateException("Server is already started");
}
// call the setRestfulServer() method to point the Conformance
// Provider to this server instance. This is done to avoid
// passing the server into the constructor. Having that sort
// of cross linkage causes reference cycles in Spring wiring
try {
Method setRestfulServer = theServerConformanceProvider.getClass().getMethod("setRestfulServer", RestfulServer.class);
if (setRestfulServer != null) {
setRestfulServer.invoke(theServerConformanceProvider, this);
}
} catch (Exception e) {
ourLog.warn("Error calling IServerConformanceProvider.setRestfulServer", e);
}
myServerConformanceProvider = theServerConformanceProvider;
}
/**
* Gets the server's name, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*
* @see RestfulServer#setServerName(String)
*/
public String getServerName() {
return myServerName;
}
/**
* Sets the server's name, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*/
public void setServerName(String theServerName) {
myServerName = theServerName;
}
public IResourceProvider getServerProfilesProvider() {
IFhirVersionServer versionServer = (IFhirVersionServer) getFhirContext().getVersion().getServerVersion();
return versionServer.createServerProfilesProvider(this);
}
/**
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*/
public String getServerVersion() {
return myServerVersion;
}
/**
* Gets the server's version, as exported in conformance profiles exported by the server. This is informational only,
* but can be helpful to set with something appropriate.
*/
public void setServerVersion(String theServerVersion) {
myServerVersion = theServerVersion;
}
@SuppressWarnings("WeakerAccess")
protected void handleRequest(RequestTypeEnum theRequestType, HttpServletRequest theRequest, HttpServletResponse theResponse) throws ServletException, IOException {
String fhirServerBase;
ServletRequestDetails requestDetails = new ServletRequestDetails();
requestDetails.setServer(this);
requestDetails.setRequestType(theRequestType);
requestDetails.setServletRequest(theRequest);
requestDetails.setServletResponse(theResponse);
theRequest.setAttribute(SERVLET_CONTEXT_ATTRIBUTE, getServletContext());
try {
/* ***********************************
* Parse out the request parameters
* ***********************************/
String requestFullPath = StringUtils.defaultString(theRequest.getRequestURI());
String servletPath = StringUtils.defaultString(theRequest.getServletPath());
StringBuffer requestUrl = theRequest.getRequestURL();
String servletContextPath = IncomingRequestAddressStrategy.determineServletContextPath(theRequest, this);
/*
* Just for debugging..
*/
if (ourLog.isTraceEnabled()) {
ourLog.trace("Request FullPath: {}", requestFullPath);
ourLog.trace("Servlet Path: {}", servletPath);
ourLog.trace("Request Url: {}", requestUrl);
ourLog.trace("Context Path: {}", servletContextPath);
}
String completeUrl;
Map<String, String[]> params = null;
if (StringUtils.isNotBlank(theRequest.getQueryString())) {
completeUrl = requestUrl + "?" + theRequest.getQueryString();
/*
* By default, we manually parse the request params (the URL params, or the body for
* POST form queries) since Java containers can't be trusted to use UTF-8 encoding
* when parsing. Specifically Tomcat 7 and Glassfish 4.0 use 8859-1 for some dumb
* reason.... grr.....
*/
if (isIgnoreServerParsedRequestParameters()) {
String contentType = theRequest.getHeader(Constants.HEADER_CONTENT_TYPE);
if (theRequestType == RequestTypeEnum.POST && isNotBlank(contentType) && contentType.startsWith(Constants.CT_X_FORM_URLENCODED)) {
String requestBody = new String(requestDetails.loadRequestContents(), Constants.CHARSET_UTF8);
params = UrlUtil.parseQueryStrings(theRequest.getQueryString(), requestBody);
} else if (theRequestType == RequestTypeEnum.GET) {
params = UrlUtil.parseQueryString(theRequest.getQueryString());
}
}
} else {
completeUrl = requestUrl.toString();
}
if (params == null) {
// If the request is coming in with a content-encoding, don't try to
// load the params from the content.
if (isNotBlank(theRequest.getHeader(Constants.HEADER_CONTENT_ENCODING))) {
if (isNotBlank(theRequest.getQueryString())) {
params = UrlUtil.parseQueryString(theRequest.getQueryString());
} else {
params = Collections.emptyMap();
}
}
if (params == null) {
params = new HashMap<>(theRequest.getParameterMap());
}
}
requestDetails.setParameters(params);
/* *************************
* Notify interceptors about the incoming request
* *************************/
for (IServerInterceptor next : myInterceptors) {
boolean continueProcessing = next.incomingRequestPreProcessed(theRequest, theResponse);
if (!continueProcessing) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return;
}
}
String requestPath = getRequestPath(requestFullPath, servletContextPath, servletPath);
if (requestPath.length() > 0 && requestPath.charAt(0) == '/') {
requestPath = requestPath.substring(1);
}
IIdType id;
populateRequestDetailsFromRequestPath(requestDetails, requestPath);
fhirServerBase = getServerBaseForRequest(requestDetails);
if (theRequestType == RequestTypeEnum.PUT) {
String contentLocation = theRequest.getHeader(Constants.HEADER_CONTENT_LOCATION);
if (contentLocation != null) {
id = myFhirContext.getVersion().newIdType();
id.setValue(contentLocation);
requestDetails.setId(id);
}
}
String acceptEncoding = theRequest.getHeader(Constants.HEADER_ACCEPT_ENCODING);
boolean respondGzip = false;
if (acceptEncoding != null) {
String[] parts = acceptEncoding.trim().split("\\s*,\\s*");
for (String string : parts) {
if (string.equals("gzip")) {
respondGzip = true;
}
}
}
requestDetails.setRespondGzip(respondGzip);
requestDetails.setRequestPath(requestPath);
requestDetails.setFhirServerBase(fhirServerBase);
requestDetails.setCompleteUrl(completeUrl);
// String pagingAction = theRequest.getParameter(Constants.PARAM_PAGINGACTION);
// if (getPagingProvider() != null && isNotBlank(pagingAction)) {
// requestDetails.setRestOperationType(RestOperationTypeEnum.GET_PAGE);
// if (theRequestType != RequestTypeEnum.GET) {
// /*
// * We reconstruct the link-self URL using the request parameters, and this would break if the parameters came
// in using a POST. We could probably work around that but why bother unless
// * someone comes up with a reason for needing it.
// */
// throw new InvalidRequestException(getFhirContext().getLocalizer().getMessage(RestfulServer.class,
// "getPagesNonHttpGet"));
// }
// handlePagingRequest(requestDetails, theResponse, pagingAction);
// return;
// }
BaseMethodBinding<?> resourceMethod = determineResourceMethod(requestDetails, requestPath);
requestDetails.setRestOperationType(resourceMethod.getRestOperationType());
// Handle server interceptors
for (IServerInterceptor next : myInterceptors) {
boolean continueProcessing = next.incomingRequestPostProcessed(requestDetails, theRequest, theResponse);
if (!continueProcessing) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return;
}
}
/*
* Actualy invoke the server method. This call is to a HAPI method binding, which
* is an object that wraps a specific implementing (user-supplied) method, but
* handles its input and provides its output back to the client.
*
* This is basically the end of processing for a successful request, since the
* method binding replies to the client and closes the response.
*/
Closeable outputStreamOrWriter = (Closeable) resourceMethod.invokeServer(this, requestDetails);
for (int i = getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = getInterceptors().get(i);
try {
next.processingCompletedNormally(requestDetails);
} catch (Throwable t) {
ourLog.error("Failure in interceptor method", t);
}
}
IOUtils.closeQuietly(outputStreamOrWriter);
} catch (NotModifiedException | AuthenticationException e) {
for (int i = getInterceptors().size() - 1; i >= 0; i--) {
IServerInterceptor next = getInterceptors().get(i);
if (!next.handleException(requestDetails, e, theRequest, theResponse)) {
ourLog.debug("Interceptor {} returned false, not continuing processing");
return;
}
}
writeExceptionToResponse(theResponse, e);
} catch (Throwable e) {
/*
* We have caught an exception during request processing. This might be because a handling method threw
* something they wanted to throw (e.g. UnprocessableEntityException because the request
* had business requirement problems) or it could be due to bugs (e.g. NullPointerException).
*