/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4chee.archive.qido;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import javax.xml.transform.stream.StreamResult;
import org.dcm4che3.conf.core.api.ConfigurationException;
import org.dcm4che3.data.Attributes;
import org.dcm4che3.data.ElementDictionary;
import org.dcm4che3.data.Sequence;
import org.dcm4che3.data.Tag;
import org.dcm4che3.data.UID;
import org.dcm4che3.data.VR;
import org.dcm4che3.io.SAXTransformer;
import org.dcm4che3.json.JSONWriter;
import org.dcm4che3.net.ApplicationEntity;
import org.dcm4che3.net.Device;
import org.dcm4che3.net.QueryOption;
import org.dcm4che3.net.TransferCapability;
import org.dcm4che3.net.TransferCapability.Role;
import org.dcm4che3.net.service.DicomServiceException;
import org.dcm4che3.net.service.QueryRetrieveLevel;
import org.dcm4che3.util.StringUtils;
import org.dcm4che3.ws.rs.MediaTypes;
import org.dcm4chee.archive.conf.ArchiveAEExtension;
import org.dcm4chee.archive.conf.QueryParam;
import org.dcm4chee.archive.query.Query;
import org.dcm4chee.archive.query.QueryContext;
import org.dcm4chee.archive.query.QueryService;
import org.dcm4chee.archive.query.QueryServiceUtils;
import org.dcm4chee.archive.query.util.QueryBuilder;
import org.dcm4chee.archive.rs.HostAECache;
import org.dcm4chee.archive.rs.HttpSource;
import org.dcm4chee.archive.web.QidoRS;
import org.jboss.resteasy.plugins.providers.multipart.MultipartRelatedOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mysema.query.types.OrderSpecifier;
import com.mysema.query.types.path.DateTimePath;
import com.mysema.query.types.path.StringPath;
/**
*
* @author Gunter Zeilinger <gunterze@gmail.com>
* @author Umberto Cappellini <umberto.cappellini@agfa.com>
* @author Alessio Roselli <alessio.roselli@agfa.com>
*/
public class DefaultQidoRS implements QidoRS {
private static final int STATUS_OK = 200;
private static final int STATUS_PARTIAL_CONTENT = 206;
private static final Logger LOG = LoggerFactory.getLogger(DefaultQidoRS.class);
private static ElementDictionary DICT =
ElementDictionary.getStandardElementDictionary();
private final static int[] STUDY_FIELDS = {
Tag.StudyDate, Tag.StudyTime, Tag.AccessionNumber,
Tag.ModalitiesInStudy, Tag.ReferringPhysicianName,
Tag.PatientName, Tag.PatientID, Tag.PatientBirthDate,
Tag.PatientSex, Tag.StudyID, Tag.StudyInstanceUID,
Tag.NumberOfStudyRelatedSeries,
Tag.NumberOfStudyRelatedInstances
};
private final static int[] SERIES_FIELDS = {
Tag.Modality, Tag.SeriesDescription, Tag.SeriesNumber,
Tag.SeriesInstanceUID, Tag.NumberOfSeriesRelatedInstances,
Tag.PerformedProcedureStepStartDate,
Tag.PerformedProcedureStepStartTime,
Tag.RequestAttributesSequence
};
private final static int[] INSTANCE_FIELDS = {
Tag.SOPClassUID, Tag.SOPInstanceUID, Tag.InstanceNumber,
Tag.Rows, Tag.Columns, Tag.BitsAllocated, Tag.NumberOfFrames
};
private final static int[] STUDY_SERIES_FIELDS =
catAndSort(STUDY_FIELDS, SERIES_FIELDS);
private final static int[] STUDY_SERIES_INSTANCE_FIELDS =
catAndSort(STUDY_SERIES_FIELDS, INSTANCE_FIELDS);
private final static int[] SERIES_INSTANCE_FIELDS =
catAndSort(SERIES_FIELDS, INSTANCE_FIELDS);
private String aetitle;
private ApplicationEntity ae;
private ArchiveAEExtension arcAE;
private QueryContext queryContext;
@Inject
private Device device;
@Inject
protected QueryService queryService;
@Inject
private HostAECache hostAECache;
@Context
private HttpServletRequest request;
@Context
private UriInfo uriInfo;
@Context
private HttpHeaders headers;
@javax.ws.rs.QueryParam("fuzzymatching")
private boolean fuzzymatching;
@javax.ws.rs.QueryParam("datetimematching")
private boolean datetimematching;
@javax.ws.rs.QueryParam("timezoneadjustment")
private boolean timezoneadjustment;
@javax.ws.rs.QueryParam("offset")
private int offset;
@javax.ws.rs.QueryParam("limit")
private int limit;
@javax.ws.rs.QueryParam("includefield")
private List<String> includefield;
@javax.ws.rs.QueryParam("orderby")
private List<String> orderby;
private OrderSpecifier<?>[] orderSpecifiers;
private final Attributes keys = new Attributes(64);
private String method;
private boolean includeAll;
private static int[] catAndSort(int[] src1, int[] src2) {
int[] dest = new int[src1.length + src2.length];
System.arraycopy(src1, 0, dest, 0, src1.length);
System.arraycopy(src2, 0, dest, src1.length, src2.length);
Arrays.sort(dest);
return dest;
}
/**
* Setter for the AETitle property, automatically invoked by the CDI
* container. Setter initializes ArchiveAEExtension and queryParam too.
*
* @param aet
* AE title
*/
@PathParam("AETitle")
public void setAETitle(String aet) {
this.aetitle=aet;
ae = device.getApplicationEntity(aet);
if (ae == null || !ae.isInstalled()
|| (arcAE = ae.getAEExtension(ArchiveAEExtension.class)) == null) {
throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE);
}
}
@Override
public Response searchForStudiesXML() throws Exception {
return search("searchForStudiesXML", QueryRetrieveLevel.STUDY,
false, null, null, STUDY_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForStudiesJSON() throws Exception {
return search("searchForStudiesJSON", QueryRetrieveLevel.STUDY,
false, null, null, STUDY_FIELDS, Output.JSON);
}
@Override
public Response searchForSeriesXML() throws Exception {
return search("searchForSeriesXML",
QueryRetrieveLevel.SERIES, true, null, null,
STUDY_SERIES_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForSeriesJSON() throws Exception {
return search("searchForSeriesJSON",
QueryRetrieveLevel.SERIES, true, null, null,
STUDY_SERIES_FIELDS, Output.JSON);
}
@Override
public Response searchForSeriesOfStudyXML(String studyInstanceUID) throws Exception {
return search("searchForSeriesOfStudyXML", QueryRetrieveLevel.SERIES,
false, studyInstanceUID, null, SERIES_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForSeriesOfStudyJSON(String studyInstanceUID) throws Exception {
return search("searchForSeriesOfStudyJSON", QueryRetrieveLevel.SERIES,
false, studyInstanceUID, null, SERIES_FIELDS, Output.JSON);
}
@Override
public Response searchForInstancesXML() throws Exception {
return search("searchForInstancesXML",
QueryRetrieveLevel.IMAGE, true, null, null,
STUDY_SERIES_INSTANCE_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForInstancesJSON() throws Exception {
return search("searchForInstancesJSON",
QueryRetrieveLevel.IMAGE, true, null, null,
STUDY_SERIES_INSTANCE_FIELDS, Output.JSON);
}
@Override
public Response searchForInstancesOfStudyXML(String studyInstanceUID) throws Exception {
return search("searchForInstancesOfStudyXML", QueryRetrieveLevel.IMAGE,
true, studyInstanceUID, null, SERIES_INSTANCE_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForInstancesOfStudyJSON(String studyInstanceUID) throws Exception {
return search("searchForInstancesOfStudyJSON", QueryRetrieveLevel.IMAGE,
true, studyInstanceUID, null, SERIES_INSTANCE_FIELDS, Output.JSON);
}
@Override
public Response searchForInstancesOfSeriesXML(String studyInstanceUID,String seriesInstanceUID) throws Exception {
return search("searchForInstancesOfSeriesXML", QueryRetrieveLevel.IMAGE,
false, studyInstanceUID, seriesInstanceUID,
INSTANCE_FIELDS, Output.DICOM_XML);
}
@Override
public Response searchForInstancesOfSeriesJSON(String studyInstanceUID,String seriesInstanceUID) throws Exception {
return search("searchForInstancesOfSeriesJSON", QueryRetrieveLevel.IMAGE,
false, studyInstanceUID, seriesInstanceUID,
INSTANCE_FIELDS, Output.JSON);
}
private Response search(String method, QueryRetrieveLevel qrlevel,
boolean relational, String studyInstanceUID,
String seriesInstanceUID, int[] includetags, Output output) throws Exception{
init(method, qrlevel, relational, studyInstanceUID, seriesInstanceUID,
includetags);
Query query = QueryServiceUtils.createQuery(queryService, qrlevel, queryContext);
try {
query.initQuery();
int status = STATUS_OK;
int maxResults = arcAE.getQIDOMaxNumberOfResults();
int offset = Math.max(this.offset, 0);
int limit = Math.max(this.limit, 0);
if (maxResults > 0 && (limit == 0 || limit > maxResults)) {
int numResults = (int) (query.count() - offset);
if (numResults == 0)
return Response.ok().build();
if (numResults > maxResults) {
limit = maxResults;
status = STATUS_PARTIAL_CONTENT;
}
}
if (offset > 0)
query.offset(offset);
if (limit > 0)
query.limit(limit);
if (orderSpecifiers != null)
query.orderBy(orderSpecifiers);
query.executeQuery();
if (!query.hasMoreMatches())
return Response.ok().build();
return Response.status(status).entity(
output.entity(this, query, qrlevel)).build();
} finally {
query.close();
}
}
/**
* Initializes query options and parameters
*
* @throws DicomServiceException
*/
private void init(String method, QueryRetrieveLevel qrlevel,
boolean relational, String studyInstanceUID,
String seriesInstanceUID, int[] defIncludefields) throws DicomServiceException {
this.method = method;
ApplicationEntity sourceAE;
try {
sourceAE = hostAECache.findAE(new HttpSource(request));
} catch (ConfigurationException e) {
throw new WebApplicationException(e, Status.INTERNAL_SERVER_ERROR);
}
TransferCapability tc = ae.getTransferCapabilityFor(
UID.StudyRootQueryRetrieveInformationModelFIND, Role.SCP);
if (tc == null)
throw new WebApplicationException(Status.FORBIDDEN);
EnumSet<QueryOption> queryOpts = EnumSet.noneOf(QueryOption.class);
if (relational)
queryOpts.add(QueryOption.RELATIONAL);
if (datetimematching)
queryOpts.add(QueryOption.DATETIME);
if (fuzzymatching)
queryOpts.add(QueryOption.FUZZY);
if (timezoneadjustment)
queryOpts.add(QueryOption.TIMEZONE);
if (!queryOpts.isEmpty()) {
EnumSet<QueryOption> supportedQueryOpts = tc.getQueryOptions();
if (supportedQueryOpts == null
|| !supportedQueryOpts.containsAll(queryOpts))
throw new WebApplicationException(Status.FORBIDDEN);
}
try {
includeAll = !includefield.isEmpty()
&& includefield.get(0).equalsIgnoreCase("all");
if (!includeAll) {
initDefaultIncludefields(defIncludefields);
parseIncludefield();
}
for (Map.Entry<String, List<String>> qParam
: uriInfo.getQueryParameters().entrySet()) {
String name = qParam.getKey();
if (isDicomAttribute(name))
parseDicomAttribute(name, qParam.getValue());
}
if (studyInstanceUID != null)
keys.setString(Tag.StudyInstanceUID, VR.UI, studyInstanceUID);
if (seriesInstanceUID != null)
keys.setString(Tag.SeriesInstanceUID, VR.UI, seriesInstanceUID);
LOG.debug("{}: Querykeys:\n{}", method, keys);
parseOrderby(qrlevel);
} catch (IllegalArgumentException e) {
throw new WebApplicationException(e, Status.BAD_REQUEST);
}
queryContext = queryService.createQueryContext(queryService);
queryContext.setRemoteAET(sourceAE.getAETitle());
queryContext.setServiceSOPClassUID(UID.StudyRootQueryRetrieveInformationModelFIND);
queryContext.setArchiveAEExtension(arcAE);
QueryParam queryParam = queryService.getQueryParam(
request, queryContext.getRemoteAET(), arcAE, queryOpts,
accessControlIDs());
queryContext.setQueryParam(queryParam);
queryContext.setKeys(keys);
queryService.coerceRequestAttributes(queryContext);
queryService.initPatientIDs(queryContext);
}
//TODO
private String[] accessControlIDs() {
return StringUtils.EMPTY_STRING;
}
private void initDefaultIncludefields(int[] defIncludefields) {
for (int tag : defIncludefields) {
keys.setNull(tag, DICT.vrOf(tag));
}
}
private static boolean isDicomAttribute(String name) {
switch (name.charAt(0)) {
case 'd':
return !name.equals("datetimematching");
case 'f':
return !name.equals("fuzzymatching");
case 'i':
return !name.equals("includefield");
case 'l':
return !name.equals("limit");
case 'o':
return !name.equals("offset")
&& !name.equals("orderby");
case 't':
return !name.equals("timezoneadjustment");
}
return true;
}
private void parseIncludefield() {
for (String s : includefield) {
for (String field : StringUtils.split(s, ',')) {
try {
int[] tagPath = parseTagPath(field);
int tag = tagPath[tagPath.length-1];
nestedKeys(tagPath).setNull(tag, DICT.vrOf(tag));
} catch (IllegalArgumentException e2) {
throw new IllegalArgumentException("includefield=" + s);
}
}
}
}
private void parseOrderby(QueryRetrieveLevel qrLevel) {
if (orderby.isEmpty())
return;
ArrayList<OrderSpecifier<?>> list = new ArrayList<OrderSpecifier<?>>();
for (String s : orderby) {
try {
for (String field : StringUtils.split(s, ',')) {
boolean desc = field.charAt(0) == '-';
int tag = parseTag(desc ? field.substring(1) : field);
for (com.mysema.query.types.Path<?> path : QueryBuilder.stringOrDateTimePathOf(tag, qrLevel)) {
if (path instanceof DateTimePath) {
list.add(desc ?
((DateTimePath<java.util.Date>) path).desc()
: ((DateTimePath<java.util.Date>) path).asc());
}
else {
list.add(desc ? ((StringPath) path).desc() : ((StringPath) path).asc());
}
}
}
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("orderby=" + s);
}
}
orderSpecifiers = list.toArray(new OrderSpecifier<?>[list.size()]);
}
private void parseDicomAttribute(String attrPath, List<String> values) {
try {
int[] tagPath = parseTagPath(attrPath);
int tag = tagPath[tagPath.length-1];
nestedKeys(tagPath).setString(tag, DICT.vrOf(tag),
values.toArray(new String[values.size()]));
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(attrPath + "=" + values.get(0));
}
}
private Attributes nestedKeys(int[] tags) {
Attributes item = keys;
for (int i = 0; i < tags.length-1; i++) {
int tag = tags[i];
Sequence sq = item.getSequence(tag);
if (sq == null)
sq = item.newSequence(tag, 1);
if (sq.isEmpty())
sq.add(new Attributes());
item = sq.get(0);
}
return item;
}
private static int[] parseTagPath(String attrPath) {
return parseTagPath(StringUtils.split(attrPath, '.'));
}
private static int[] parseTagPath(String[] attrPath) {
int[] tags = new int[attrPath.length];
for (int i = 0; i < tags.length; i++)
tags[i] = parseTag(attrPath[i]);
return tags;
}
private static int parseTag(String tagOrKeyword) {
try {
return Integer.parseInt(tagOrKeyword, 16);
} catch (IllegalArgumentException e) {
int tag = DICT.tagForKeyword(tagOrKeyword);
if (tag == -1)
throw new IllegalArgumentException(tagOrKeyword);
return tag;
}
}
private enum Output {
DICOM_XML {
@Override
Object entity(DefaultQidoRS service, Query query, QueryRetrieveLevel qrlevel) {
return service.writeXML(query, qrlevel);
}
},
JSON {
@Override
Object entity(DefaultQidoRS service, Query query, QueryRetrieveLevel qrlevel) {
return service.writeJSON(query, qrlevel);
}
};
abstract Object entity(DefaultQidoRS service, Query query, QueryRetrieveLevel qrlevel);
}
private Object writeXML(Query query, QueryRetrieveLevel qrlevel) {
MultipartRelatedOutput output = new MultipartRelatedOutput();
int count = 0;
while (query.hasMoreMatches()) {
Attributes tmp = query.nextMatch();
if (tmp == null)
continue;
final Attributes match = adjust(tmp, qrlevel, query);
LOG.debug("{}: Match #{}:\n{}", new Object[]{method, ++count, match});
output.addPart(new StreamingOutput() {
@Override
public void write(OutputStream out) throws IOException,
WebApplicationException {
try {
SAXTransformer.getSAXWriter(new StreamResult(out)).write(match);
} catch (Exception e) {
throw new WebApplicationException(e);
}
}},
MediaTypes.APPLICATION_DICOM_XML_TYPE);
}
LOG.info("{}: {} Matches", method, count);
return output;
}
private Object writeJSON(Query query, QueryRetrieveLevel qrlevel) {
final ArrayList<Attributes> matches = new ArrayList<Attributes>();
int count = 0;
while (query.hasMoreMatches()) {
Attributes tmp = query.nextMatch();
if (tmp == null)
continue;
Attributes match = adjust(tmp, qrlevel, query);
LOG.debug("{}: Match #{}:\n{}", new Object[]{method, ++count, match});
matches.add(match);
}
LOG.info("{}: {} Matches", method, count);
StreamingOutput output = new StreamingOutput(){
@Override
public void write(OutputStream out) throws IOException {
try {
JsonGenerator gen = Json.createGenerator(out);
JSONWriter writer = new JSONWriter(gen);
gen.writeStartArray();
for (int i = 0, n=matches.size(); i < n; i++) {
Attributes match = matches.get(i);
writer.write(match);
}
gen.writeEnd();
gen.flush();
} catch (Exception e) {
throw new WebApplicationException(e);
}
}
};
return output;
}
private Attributes adjust(Attributes match, QueryRetrieveLevel qrlevel, Query query) {
// response adjustment (e.g. timezone)
try {
queryService.coerceResponseAttributes(query.getQueryContext(), match);
} catch (DicomServiceException e) {
throw new WebApplicationException(e);
}
return filter(addRetrieveURL(match, qrlevel));
}
private Attributes addRetrieveURL(Attributes match, QueryRetrieveLevel qrlevel) {
match.setString(Tag.RetrieveURL, VR.UR, RetrieveURL(match, qrlevel));
return match;
}
private String RetrieveURL(Attributes match, QueryRetrieveLevel qrlevel) {
StringBuilder sb = new StringBuilder(256);
sb.append(uriInfo.getBaseUri())
.append(aetitle)
.append("/studies/")
.append(match.getString(Tag.StudyInstanceUID));
if (qrlevel == QueryRetrieveLevel.STUDY)
return sb.toString();
sb.append("/series/")
.append(match.getString(Tag.SeriesInstanceUID));
if (qrlevel == QueryRetrieveLevel.SERIES)
return sb.toString();
sb.append("/instances/")
.append(match.getString(Tag.SOPInstanceUID));
return sb.toString();
}
private Attributes filter(Attributes match) {
if (includeAll)
return match;
Attributes filtered = new Attributes(match.size());
filtered.addSelected(match, Tag.SpecificCharacterSet,
Tag.RetrieveAETitle, Tag.InstanceAvailability, Tag.RetrieveURL);
filtered.addSelected(match, keys);
return filtered;
}
}