Mercurial > hg > orthanc
comparison OrthancServer/OrthancRestApi.cpp @ 229:f2349bab5fe7
rename
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Fri, 30 Nov 2012 11:00:40 +0100 |
parents | OrthancServer/OrthancRestApi2.cpp@209ca3f6db62 |
children | ae2367145b49 |
comparison
equal
deleted
inserted
replaced
228:1af3bc092db8 | 229:f2349bab5fe7 |
---|---|
1 /** | |
2 * Orthanc - A Lightweight, RESTful DICOM Store | |
3 * Copyright (C) 2012 Medical Physics Department, CHU of Liege, | |
4 * Belgium | |
5 * | |
6 * This program is free software: you can redistribute it and/or | |
7 * modify it under the terms of the GNU General Public License as | |
8 * published by the Free Software Foundation, either version 3 of the | |
9 * License, or (at your option) any later version. | |
10 * | |
11 * In addition, as a special exception, the copyright holders of this | |
12 * program give permission to link the code of its release with the | |
13 * OpenSSL project's "OpenSSL" library (or with modified versions of it | |
14 * that use the same license as the "OpenSSL" library), and distribute | |
15 * the linked executables. You must obey the GNU General Public License | |
16 * in all respects for all of the code used other than "OpenSSL". If you | |
17 * modify file(s) with this exception, you may extend this exception to | |
18 * your version of the file(s), but you are not obligated to do so. If | |
19 * you do not wish to do so, delete this exception statement from your | |
20 * version. If you delete this exception statement from all source files | |
21 * in the program, then also delete it here. | |
22 * | |
23 * This program is distributed in the hope that it will be useful, but | |
24 * WITHOUT ANY WARRANTY; without even the implied warranty of | |
25 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
26 * General Public License for more details. | |
27 * | |
28 * You should have received a copy of the GNU General Public License | |
29 * along with this program. If not, see <http://www.gnu.org/licenses/>. | |
30 **/ | |
31 | |
32 | |
33 #include "OrthancRestApi2.h" | |
34 | |
35 #include "../Core/HttpServer/FilesystemHttpSender.h" | |
36 #include "../Core/Uuid.h" | |
37 #include "DicomProtocol/DicomUserConnection.h" | |
38 #include "FromDcmtkBridge.h" | |
39 #include "OrthancInitialization.h" | |
40 #include "ServerToolbox.h" | |
41 | |
42 #include <dcmtk/dcmdata/dcistrmb.h> | |
43 #include <dcmtk/dcmdata/dcfilefo.h> | |
44 #include <boost/lexical_cast.hpp> | |
45 #include <glog/logging.h> | |
46 | |
47 | |
48 #define RETRIEVE_CONTEXT(call) \ | |
49 OrthancRestApi2& contextApi = \ | |
50 dynamic_cast<OrthancRestApi2&>(call.GetContext()); \ | |
51 ServerContext& context = contextApi.GetContext() | |
52 | |
53 #define RETRIEVE_MODALITIES(call) \ | |
54 const OrthancRestApi2::Modalities& modalities = \ | |
55 dynamic_cast<OrthancRestApi2&>(call.GetContext()).GetModalities(); | |
56 | |
57 | |
58 | |
59 namespace Orthanc | |
60 { | |
61 // DICOM SCU ---------------------------------------------------------------- | |
62 | |
63 static void ConnectToModality(DicomUserConnection& connection, | |
64 const std::string& name) | |
65 { | |
66 std::string aet, address; | |
67 int port; | |
68 GetDicomModality(name, aet, address, port); | |
69 connection.SetLocalApplicationEntityTitle(GetGlobalStringParameter("DicomAet", "ORTHANC")); | |
70 connection.SetDistantApplicationEntityTitle(aet); | |
71 connection.SetDistantHost(address); | |
72 connection.SetDistantPort(port); | |
73 connection.Open(); | |
74 } | |
75 | |
76 static bool MergeQueryAndTemplate(DicomMap& result, | |
77 const std::string& postData) | |
78 { | |
79 Json::Value query; | |
80 Json::Reader reader; | |
81 | |
82 if (!reader.parse(postData, query) || | |
83 query.type() != Json::objectValue) | |
84 { | |
85 return false; | |
86 } | |
87 | |
88 Json::Value::Members members = query.getMemberNames(); | |
89 for (size_t i = 0; i < members.size(); i++) | |
90 { | |
91 DicomTag t = FromDcmtkBridge::FindTag(members[i]); | |
92 result.SetValue(t, query[members[i]].asString()); | |
93 } | |
94 | |
95 return true; | |
96 } | |
97 | |
98 static void DicomFindPatient(RestApi::PostCall& call) | |
99 { | |
100 DicomMap m; | |
101 DicomMap::SetupFindPatientTemplate(m); | |
102 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
103 { | |
104 return; | |
105 } | |
106 | |
107 DicomUserConnection connection; | |
108 ConnectToModality(connection, call.GetUriComponent("id", "")); | |
109 | |
110 DicomFindAnswers answers; | |
111 connection.FindPatient(answers, m); | |
112 | |
113 Json::Value result; | |
114 answers.ToJson(result); | |
115 call.GetOutput().AnswerJson(result); | |
116 } | |
117 | |
118 static void DicomFindStudy(RestApi::PostCall& call) | |
119 { | |
120 DicomMap m; | |
121 DicomMap::SetupFindStudyTemplate(m); | |
122 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
123 { | |
124 return; | |
125 } | |
126 | |
127 if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && | |
128 m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) | |
129 { | |
130 return; | |
131 } | |
132 | |
133 DicomUserConnection connection; | |
134 ConnectToModality(connection, call.GetUriComponent("id", "")); | |
135 | |
136 DicomFindAnswers answers; | |
137 connection.FindStudy(answers, m); | |
138 | |
139 Json::Value result; | |
140 answers.ToJson(result); | |
141 call.GetOutput().AnswerJson(result); | |
142 } | |
143 | |
144 static void DicomFindSeries(RestApi::PostCall& call) | |
145 { | |
146 DicomMap m; | |
147 DicomMap::SetupFindSeriesTemplate(m); | |
148 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
149 { | |
150 return; | |
151 } | |
152 | |
153 if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 && | |
154 m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) || | |
155 m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2) | |
156 { | |
157 return; | |
158 } | |
159 | |
160 DicomUserConnection connection; | |
161 ConnectToModality(connection, call.GetUriComponent("id", "")); | |
162 | |
163 DicomFindAnswers answers; | |
164 connection.FindSeries(answers, m); | |
165 | |
166 Json::Value result; | |
167 answers.ToJson(result); | |
168 call.GetOutput().AnswerJson(result); | |
169 } | |
170 | |
171 static void DicomFind(RestApi::PostCall& call) | |
172 { | |
173 DicomMap m; | |
174 DicomMap::SetupFindPatientTemplate(m); | |
175 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
176 { | |
177 return; | |
178 } | |
179 | |
180 DicomUserConnection connection; | |
181 ConnectToModality(connection, call.GetUriComponent("id", "")); | |
182 | |
183 DicomFindAnswers patients; | |
184 connection.FindPatient(patients, m); | |
185 | |
186 // Loop over the found patients | |
187 Json::Value result = Json::arrayValue; | |
188 for (size_t i = 0; i < patients.GetSize(); i++) | |
189 { | |
190 Json::Value patient(Json::objectValue); | |
191 FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i)); | |
192 | |
193 DicomMap::SetupFindStudyTemplate(m); | |
194 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
195 { | |
196 return; | |
197 } | |
198 m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID); | |
199 | |
200 DicomFindAnswers studies; | |
201 connection.FindStudy(studies, m); | |
202 | |
203 patient["Studies"] = Json::arrayValue; | |
204 | |
205 // Loop over the found studies | |
206 for (size_t j = 0; j < studies.GetSize(); j++) | |
207 { | |
208 Json::Value study(Json::objectValue); | |
209 FromDcmtkBridge::ToJson(study, studies.GetAnswer(j)); | |
210 | |
211 DicomMap::SetupFindSeriesTemplate(m); | |
212 if (!MergeQueryAndTemplate(m, call.GetPostBody())) | |
213 { | |
214 return; | |
215 } | |
216 m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_PATIENT_ID); | |
217 m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID); | |
218 | |
219 DicomFindAnswers series; | |
220 connection.FindSeries(series, m); | |
221 | |
222 // Loop over the found series | |
223 study["Series"] = Json::arrayValue; | |
224 for (size_t k = 0; k < series.GetSize(); k++) | |
225 { | |
226 Json::Value series2(Json::objectValue); | |
227 FromDcmtkBridge::ToJson(series2, series.GetAnswer(k)); | |
228 study["Series"].append(series2); | |
229 } | |
230 | |
231 patient["Studies"].append(study); | |
232 } | |
233 | |
234 result.append(patient); | |
235 } | |
236 | |
237 call.GetOutput().AnswerJson(result); | |
238 } | |
239 | |
240 | |
241 static void DicomStore(RestApi::PostCall& call) | |
242 { | |
243 RETRIEVE_CONTEXT(call); | |
244 | |
245 DicomUserConnection connection; | |
246 ConnectToModality(connection, call.GetUriComponent("id", "")); | |
247 | |
248 Json::Value found; | |
249 if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Series)) | |
250 { | |
251 // The UUID corresponds to a series | |
252 for (Json::Value::ArrayIndex i = 0; i < found["Instances"].size(); i++) | |
253 { | |
254 std::string instanceId = found["Instances"][i].asString(); | |
255 std::string dicom; | |
256 context.ReadFile(dicom, instanceId, AttachedFileType_Dicom); | |
257 connection.Store(dicom); | |
258 } | |
259 | |
260 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
261 } | |
262 else if (context.GetIndex().LookupResource(found, call.GetPostBody(), ResourceType_Instance)) | |
263 { | |
264 // The UUID corresponds to an instance | |
265 std::string instanceId = call.GetPostBody(); | |
266 std::string dicom; | |
267 context.ReadFile(dicom, instanceId, AttachedFileType_Dicom); | |
268 connection.Store(dicom); | |
269 | |
270 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
271 } | |
272 else | |
273 { | |
274 // The POST body is not a known resource, assume that it | |
275 // contains a raw DICOM instance | |
276 connection.Store(call.GetPostBody()); | |
277 call.GetOutput().AnswerBuffer("{}", "application/json"); | |
278 } | |
279 } | |
280 | |
281 | |
282 | |
283 // System information ------------------------------------------------------- | |
284 | |
285 static void ServeRoot(RestApi::GetCall& call) | |
286 { | |
287 call.GetOutput().Redirect("app/explorer.html"); | |
288 } | |
289 | |
290 static void GetSystemInformation(RestApi::GetCall& call) | |
291 { | |
292 RETRIEVE_CONTEXT(call); | |
293 | |
294 Json::Value result = Json::objectValue; | |
295 result["Version"] = ORTHANC_VERSION; | |
296 result["Name"] = GetGlobalStringParameter("Name", ""); | |
297 result["TotalCompressedSize"] = boost::lexical_cast<std::string> | |
298 (context.GetIndex().GetTotalCompressedSize()); | |
299 result["TotalUncompressedSize"] = boost::lexical_cast<std::string> | |
300 (context.GetIndex().GetTotalUncompressedSize()); | |
301 | |
302 call.GetOutput().AnswerJson(result); | |
303 } | |
304 | |
305 | |
306 // List all the patients, studies, series or instances ---------------------- | |
307 | |
308 template <enum ResourceType resourceType> | |
309 static void ListResources(RestApi::GetCall& call) | |
310 { | |
311 RETRIEVE_CONTEXT(call); | |
312 | |
313 Json::Value result; | |
314 context.GetIndex().GetAllUuids(result, resourceType); | |
315 call.GetOutput().AnswerJson(result); | |
316 } | |
317 | |
318 template <enum ResourceType resourceType> | |
319 static void GetSingleResource(RestApi::GetCall& call) | |
320 { | |
321 RETRIEVE_CONTEXT(call); | |
322 | |
323 Json::Value result; | |
324 if (context.GetIndex().LookupResource(result, call.GetUriComponent("id", ""), resourceType)) | |
325 { | |
326 call.GetOutput().AnswerJson(result); | |
327 } | |
328 } | |
329 | |
330 template <enum ResourceType resourceType> | |
331 static void DeleteSingleResource(RestApi::DeleteCall& call) | |
332 { | |
333 RETRIEVE_CONTEXT(call); | |
334 | |
335 Json::Value result; | |
336 if (context.GetIndex().DeleteResource(result, call.GetUriComponent("id", ""), resourceType)) | |
337 { | |
338 call.GetOutput().AnswerJson(result); | |
339 } | |
340 } | |
341 | |
342 | |
343 // Changes API -------------------------------------------------------------- | |
344 | |
345 static void GetChanges(RestApi::GetCall& call) | |
346 { | |
347 RETRIEVE_CONTEXT(call); | |
348 | |
349 static const unsigned int MAX_RESULTS = 100; | |
350 ServerIndex& index = context.GetIndex(); | |
351 | |
352 //std::string filter = GetArgument(getArguments, "filter", ""); | |
353 int64_t since; | |
354 unsigned int limit; | |
355 try | |
356 { | |
357 since = boost::lexical_cast<int64_t>(call.GetArgument("since", "0")); | |
358 limit = boost::lexical_cast<unsigned int>(call.GetArgument("limit", "0")); | |
359 } | |
360 catch (boost::bad_lexical_cast) | |
361 { | |
362 return; | |
363 } | |
364 | |
365 if (limit == 0 || limit > MAX_RESULTS) | |
366 { | |
367 limit = MAX_RESULTS; | |
368 } | |
369 | |
370 Json::Value result; | |
371 if (index.GetChanges(result, since, limit)) | |
372 { | |
373 call.GetOutput().AnswerJson(result); | |
374 } | |
375 } | |
376 | |
377 | |
378 | |
379 // Get information about a single instance ---------------------------------- | |
380 | |
381 static void GetInstanceFile(RestApi::GetCall& call) | |
382 { | |
383 RETRIEVE_CONTEXT(call); | |
384 | |
385 std::string publicId = call.GetUriComponent("id", ""); | |
386 context.AnswerFile(call.GetOutput(), publicId, AttachedFileType_Dicom); | |
387 } | |
388 | |
389 | |
390 template <bool simplify> | |
391 static void GetInstanceTags(RestApi::GetCall& call) | |
392 { | |
393 RETRIEVE_CONTEXT(call); | |
394 | |
395 std::string publicId = call.GetUriComponent("id", ""); | |
396 | |
397 Json::Value full; | |
398 context.ReadJson(full, publicId); | |
399 | |
400 if (simplify) | |
401 { | |
402 Json::Value simplified; | |
403 SimplifyTags(simplified, full); | |
404 call.GetOutput().AnswerJson(simplified); | |
405 } | |
406 else | |
407 { | |
408 call.GetOutput().AnswerJson(full); | |
409 } | |
410 } | |
411 | |
412 | |
413 static void ListFrames(RestApi::GetCall& call) | |
414 { | |
415 RETRIEVE_CONTEXT(call); | |
416 | |
417 Json::Value instance; | |
418 if (context.GetIndex().LookupResource(instance, call.GetUriComponent("id", ""), ResourceType_Instance)) | |
419 { | |
420 unsigned int numberOfFrames = 1; | |
421 | |
422 try | |
423 { | |
424 Json::Value tmp = instance["MainDicomTags"]["NumberOfFrames"]; | |
425 numberOfFrames = boost::lexical_cast<unsigned int>(tmp.asString()); | |
426 } | |
427 catch (...) | |
428 { | |
429 } | |
430 | |
431 Json::Value result = Json::arrayValue; | |
432 for (unsigned int i = 0; i < numberOfFrames; i++) | |
433 { | |
434 result.append(i); | |
435 } | |
436 | |
437 call.GetOutput().AnswerJson(result); | |
438 } | |
439 } | |
440 | |
441 | |
442 template <enum ImageExtractionMode mode> | |
443 static void GetImage(RestApi::GetCall& call) | |
444 { | |
445 RETRIEVE_CONTEXT(call); | |
446 | |
447 std::string frameId = call.GetUriComponent("frame", "0"); | |
448 | |
449 unsigned int frame; | |
450 try | |
451 { | |
452 frame = boost::lexical_cast<unsigned int>(frameId); | |
453 } | |
454 catch (boost::bad_lexical_cast) | |
455 { | |
456 return; | |
457 } | |
458 | |
459 std::string publicId = call.GetUriComponent("id", ""); | |
460 std::string dicomContent, png; | |
461 context.ReadFile(dicomContent, publicId, AttachedFileType_Dicom); | |
462 | |
463 try | |
464 { | |
465 FromDcmtkBridge::ExtractPngImage(png, dicomContent, frame, mode); | |
466 call.GetOutput().AnswerBuffer(png, "image/png"); | |
467 } | |
468 catch (OrthancException& e) | |
469 { | |
470 if (e.GetErrorCode() == ErrorCode_ParameterOutOfRange) | |
471 { | |
472 // The frame number is out of the range for this DICOM | |
473 // instance, the resource is not existent | |
474 } | |
475 else | |
476 { | |
477 std::string root = ""; | |
478 for (size_t i = 1; i < call.GetFullUri().size(); i++) | |
479 { | |
480 root += "../"; | |
481 } | |
482 | |
483 call.GetOutput().Redirect(root + "app/images/unsupported.png"); | |
484 } | |
485 } | |
486 } | |
487 | |
488 | |
489 // Upload of DICOM files through HTTP --------------------------------------- | |
490 | |
491 static void UploadDicomFile(RestApi::PostCall& call) | |
492 { | |
493 RETRIEVE_CONTEXT(call); | |
494 | |
495 const std::string& postData = call.GetPostBody(); | |
496 | |
497 LOG(INFO) << "Receiving a DICOM file of " << postData.size() << " bytes through HTTP"; | |
498 | |
499 // Prepare an input stream for the memory buffer | |
500 DcmInputBufferStream is; | |
501 if (postData.size() > 0) | |
502 { | |
503 is.setBuffer(&postData[0], postData.size()); | |
504 } | |
505 is.setEos(); | |
506 | |
507 DcmFileFormat dicomFile; | |
508 if (!dicomFile.read(is).good()) | |
509 { | |
510 call.GetOutput().SignalError(Orthanc_HttpStatus_415_UnsupportedMediaType); | |
511 return; | |
512 } | |
513 | |
514 DicomMap dicomSummary; | |
515 FromDcmtkBridge::Convert(dicomSummary, *dicomFile.getDataset()); | |
516 | |
517 DicomInstanceHasher hasher(dicomSummary); | |
518 | |
519 Json::Value dicomJson; | |
520 FromDcmtkBridge::ToJson(dicomJson, *dicomFile.getDataset()); | |
521 | |
522 StoreStatus status = StoreStatus_Failure; | |
523 if (postData.size() > 0) | |
524 { | |
525 status = context.Store | |
526 (reinterpret_cast<const char*>(&postData[0]), | |
527 postData.size(), dicomSummary, dicomJson, ""); | |
528 } | |
529 | |
530 Json::Value result = Json::objectValue; | |
531 | |
532 if (status != StoreStatus_Failure) | |
533 { | |
534 result["ID"] = hasher.HashInstance(); | |
535 result["Path"] = GetBasePath(ResourceType_Instance, hasher.HashInstance()); | |
536 } | |
537 | |
538 result["Status"] = ToString(status); | |
539 call.GetOutput().AnswerJson(result); | |
540 } | |
541 | |
542 | |
543 // DICOM bridge ------------------------------------------------------------- | |
544 | |
545 static bool IsExistingModality(const OrthancRestApi2::Modalities& modalities, | |
546 const std::string& id) | |
547 { | |
548 return modalities.find(id) != modalities.end(); | |
549 } | |
550 | |
551 static void ListModalities(RestApi::GetCall& call) | |
552 { | |
553 RETRIEVE_MODALITIES(call); | |
554 | |
555 Json::Value result = Json::arrayValue; | |
556 for (OrthancRestApi2::Modalities::const_iterator | |
557 it = modalities.begin(); it != modalities.end(); it++) | |
558 { | |
559 result.append(*it); | |
560 } | |
561 | |
562 call.GetOutput().AnswerJson(result); | |
563 } | |
564 | |
565 | |
566 static void ListModalityOperations(RestApi::GetCall& call) | |
567 { | |
568 RETRIEVE_MODALITIES(call); | |
569 | |
570 std::string id = call.GetUriComponent("id", ""); | |
571 if (IsExistingModality(modalities, id)) | |
572 { | |
573 Json::Value result = Json::arrayValue; | |
574 result.append("find-patient"); | |
575 result.append("find-study"); | |
576 result.append("find-series"); | |
577 result.append("find"); | |
578 result.append("store"); | |
579 call.GetOutput().AnswerJson(result); | |
580 } | |
581 } | |
582 | |
583 | |
584 | |
585 // Registration of the various REST handlers -------------------------------- | |
586 | |
587 OrthancRestApi2::OrthancRestApi2(ServerContext& context) : | |
588 context_(context) | |
589 { | |
590 GetListOfDicomModalities(modalities_); | |
591 | |
592 Register("/", ServeRoot); | |
593 Register("/system", GetSystemInformation); | |
594 Register("/changes", GetChanges); | |
595 | |
596 Register("/instances", UploadDicomFile); | |
597 Register("/instances", ListResources<ResourceType_Instance>); | |
598 Register("/patients", ListResources<ResourceType_Patient>); | |
599 Register("/series", ListResources<ResourceType_Series>); | |
600 Register("/studies", ListResources<ResourceType_Study>); | |
601 | |
602 Register("/instances/{id}", DeleteSingleResource<ResourceType_Instance>); | |
603 Register("/instances/{id}", GetSingleResource<ResourceType_Instance>); | |
604 Register("/patients/{id}", DeleteSingleResource<ResourceType_Patient>); | |
605 Register("/patients/{id}", GetSingleResource<ResourceType_Patient>); | |
606 Register("/series/{id}", DeleteSingleResource<ResourceType_Series>); | |
607 Register("/series/{id}", GetSingleResource<ResourceType_Series>); | |
608 Register("/studies/{id}", DeleteSingleResource<ResourceType_Study>); | |
609 Register("/studies/{id}", GetSingleResource<ResourceType_Study>); | |
610 | |
611 Register("/instances/{id}/file", GetInstanceFile); | |
612 Register("/instances/{id}/tags", GetInstanceTags<false>); | |
613 Register("/instances/{id}/simplified-tags", GetInstanceTags<true>); | |
614 Register("/instances/{id}/frames", ListFrames); | |
615 | |
616 Register("/instances/{id}/frames/{frame}/preview", GetImage<ImageExtractionMode_Preview>); | |
617 Register("/instances/{id}/frames/{frame}/image-uint8", GetImage<ImageExtractionMode_UInt8>); | |
618 Register("/instances/{id}/frames/{frame}/image-uint16", GetImage<ImageExtractionMode_UInt16>); | |
619 Register("/instances/{id}/preview", GetImage<ImageExtractionMode_Preview>); | |
620 Register("/instances/{id}/image-uint8", GetImage<ImageExtractionMode_UInt8>); | |
621 Register("/instances/{id}/image-uint16", GetImage<ImageExtractionMode_UInt16>); | |
622 | |
623 Register("/modalities", ListModalities); | |
624 Register("/modalities/{id}", ListModalityOperations); | |
625 Register("/modalities/{id}/find-patient", DicomFindPatient); | |
626 Register("/modalities/{id}/find-study", DicomFindStudy); | |
627 Register("/modalities/{id}/find-series", DicomFindSeries); | |
628 Register("/modalities/{id}/find", DicomFind); | |
629 Register("/modalities/{id}/store", DicomStore); | |
630 | |
631 // TODO : "content" | |
632 } | |
633 } |