/* ***** 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) 2013 * 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.qc.rest; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import javax.inject.Inject; import javax.json.Json; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; import org.dcm4che3.data.Attributes; import org.dcm4che3.data.Code; import org.dcm4che3.data.ElementDictionary; import org.dcm4che3.data.IDWithIssuer; import org.dcm4che3.data.Issuer; import org.dcm4che3.json.JSONReader; import org.dcm4che3.json.JSONReader.Callback; import org.dcm4che3.net.ApplicationEntity; import org.dcm4che3.net.Device; import org.dcm4che3.util.TagUtils; import org.dcm4chee.archive.conf.ArchiveAEExtension; import org.dcm4chee.archive.conf.ArchiveDeviceExtension; import org.dcm4chee.archive.qc.PatientCommands; import org.dcm4chee.archive.qc.QCOperationContext; import org.dcm4chee.archive.qc.QCOperationNotPermittedException; import org.dcm4chee.archive.qc.QCService; import org.dcm4chee.archive.qc.StructuralChangeService; import org.dcm4chee.archive.sc.STRUCTURAL_CHANGE; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The Class QCRestful provides a rest interface for QC. * * @author Hesham Elbadawi <bsdreko@gmail.com> */ @Path("/qc/{AETitle}") public class QCRestful { private static final Logger LOG = LoggerFactory.getLogger(QCRestful.class); private static String RSP; @Inject private Device device; @Inject private QCService qcService; @Inject private StructuralChangeService scService; private String aeTitle; private ArchiveAEExtension arcAEExt; /** * Sets the AE title. * * @param aet * the new AE title */ @PathParam("AETitle") public void setAETitle(String aet) { this.aeTitle=aet; ApplicationEntity ae = device.getApplicationEntity(aet); if (ae == null || !ae.isInstalled() || (arcAEExt = ae.getAEExtension(ArchiveAEExtension.class)) == null) { throw new WebApplicationException(Response.Status.SERVICE_UNAVAILABLE); } } /** * Performs a QC operation based on the provided object. * * @param object * the QCObject generated by the rest call * @return the response */ @POST @Consumes("application/json") public Response performQC(QCObject object) { Code code = (object.getQcRejectionCode().getCodeValue()!=null?initializeCode(object):null); IDWithIssuer pid = (object.getPid()!=null?initializeIDWithIssuer(object):null); QCOperationContext qcOperationContext = null; try { switch (object.getOperation().toLowerCase()) { case "update": ArchiveDeviceExtension arcDevExt = device.getDeviceExtension(ArchiveDeviceExtension.class); if (object.getUpdateScope() == null) { LOG.error("Unable to decide update Scope for QC update"); throw new WebApplicationException(Response.status(Status.CONFLICT) .entity("Unrecognized update scope").build()); } else { // here merge provided data with pid, study uid, series uid // , instance uid qcOperationContext = qcService.updateDicomObject(arcDevExt, object.getUpdateScope(), object.getUpdateData()); } break; case "merge": qcOperationContext = qcService.mergeStudies(object.getMoveSOPUIDs(), object.getTargetStudyUID(), object.getTargetStudyData(), object.getTargetSeriesData(), code); break; case "split": qcOperationContext = qcService.split(Arrays.asList(object.getMoveSOPUIDs()), pid, object.getTargetStudyUID(), object.getTargetStudyData(), object.getTargetSeriesData(), code); break; case "segment": qcOperationContext = qcService.segment(Arrays.asList(object.getMoveSOPUIDs()), Arrays.asList(object.getCloneSOPUIDs()), pid, object.getTargetStudyUID(), object.getTargetStudyData(), object.getTargetSeriesData(), code); break; case "reject": qcOperationContext = scService.reject(STRUCTURAL_CHANGE.QC, object.getRestoreOrRejectUIDs(), code); break; case "restore": qcOperationContext = qcService.restore(object.getRestoreOrRejectUIDs()); break; default: return Response.status(Response.Status.CONFLICT) .entity("Unable to decide operation").build(); } } catch (Exception e) { LOG.error("{} : Error in performing QC - Restful interface", e); throw new WebApplicationException(Response.status(Status.CONFLICT) .entity(e.getMessage()).build()); } String strEvent = qcOperationContext.toString(); return Response.ok("Successfully performed operation "+object.getOperation() + " Operation resulted in the following event :\n" +strEvent).build(); } /** * Patient operation. * Calls the patient service to perform operations. * * @param uriInfo * the uri info * @param in * the in * @param patientOperation * the patient operation * @return the response */ @POST @Path("patients/{PatientOperation}") @Consumes("application/json") public Response patientOperation(@Context UriInfo uriInfo, InputStream in, @PathParam("PatientOperation") String patientOperation) { PatientCommands command = patientOperation.equalsIgnoreCase("merge")? PatientCommands.PATIENT_MERGE : patientOperation.equalsIgnoreCase("link")? PatientCommands.PATIENT_LINK : patientOperation.equalsIgnoreCase("unlink")? PatientCommands.PATIENT_UNLINK : patientOperation.equalsIgnoreCase("updateids")? PatientCommands.PATIENT_UPDATE_ID : null; if (command == null) throw new WebApplicationException( Response.status(Status.CONFLICT) .entity("Unable to decide patient command - supported commands {merge, link, unlink, updateids}") .build() ); try { ArrayList<Attributes> attrs = parseJSONAttributesToList(in); if (LOG.isDebugEnabled()) { LOG.debug("Received Attributes for patient operation - "+patientOperation); for(int i=0; i< attrs.size();i++){ LOG.debug(i%2==0 ? "Source data[{}]: ":"Target data[{}]: ",(i/2)+1); LOG.debug(attrs.get(i).toString(0, attrs.get(i).size())); } } return aggregatePatientOpResponse(attrs,device.getApplicationEntity(aeTitle),command, patientOperation); } catch(Exception e) { throw new WebApplicationException( Response.status(Status.CONFLICT) .entity("Unable to process patient operation request data") .build() ); } } /** * Delete patient. * * @param patientid * the patient id * @param issuer * the issue string separated by colons * localID:universalID:universalIDtype * @return the response */ @DELETE @Path("delete/patient/{PatientID}/issuer/{IssuerID}") public Response deletePatient( @PathParam("PatientID") String patientID, @PathParam("IssuerID") String issuerID, @QueryParam("qcRejectionCode") Code qcRejectionCode) { RSP = "Deleted Patient with PID = "; try { qcService.deletePatient(new IDWithIssuer(patientID, new Issuer(issuerID, ':')), checkRejectionCode(qcRejectionCode)); } catch (Exception e) { RSP = "Failed to delete patient with ID = "+patientID + " issued by " + issuerID; return Response.status(Status.CONFLICT).entity(RSP).build(); } RSP += patientID + " Issuer = " + issuerID; return Response.ok(RSP).build(); } /** * Delete study. * * @param studyInstanceUID * the study instance uid * @return the response */ @DELETE @Path("delete/studies/{StudyInstanceUID}") public Response deleteStudy( @PathParam("StudyInstanceUID") String studyInstanceUID, @QueryParam("qcRejectionCode") Code qcRejectionCode) { RSP = "Deleted Study with UID = "; try{ qcService.deleteStudy(studyInstanceUID, checkRejectionCode(qcRejectionCode)); } catch (Exception e) { RSP = "Failed to delete study with UID = "+studyInstanceUID; return Response.status(Status.CONFLICT).entity(RSP).build(); } RSP += studyInstanceUID; return Response.ok(RSP).build(); } /** * Delete series. * * @param studyInstanceUID * the study instance uid * @param seriesInstanceUID * the series instance uid * @return the response */ @DELETE @Path("delete/studies/{StudyInstanceUID}/series/{SeriesInstanceUID}") public Response deleteSeries( @PathParam("StudyInstanceUID") String studyInstanceUID, @PathParam("SeriesInstanceUID") String seriesInstanceUID, @QueryParam("qcRejectionCode") Code qcRejectionCode) { RSP = "Deleted Series with UID = "; try { qcService.deleteSeries(seriesInstanceUID, checkRejectionCode(qcRejectionCode)); } catch(Exception e) { RSP = "Failed to delete series with UID = "+seriesInstanceUID; return Response.status(Status.CONFLICT).entity(RSP).build(); } RSP+=seriesInstanceUID; return Response.ok(RSP).build(); } /** * Delete instance. * * @param studyInstanceUID * the study instance uid * @param seriesInstanceUID * the series instance uid * @param sopInstanceUID * the sop instance uid * @return the response */ @DELETE @Path("delete/studies/{StudyInstanceUID}/series/{SeriesInstanceUID}/instances/{SOPInstanceUID}") public Response deleteInstance( @PathParam("StudyInstanceUID") String studyInstanceUID, @PathParam("SeriesInstanceUID") String seriesInstanceUID, @PathParam("SOPInstanceUID") String sopInstanceUID, @QueryParam("qcRejectionCode") Code qcRejectionCode) { RSP = "Deleted Instance with UID = "; try{ qcService.deleteInstance(sopInstanceUID, checkRejectionCode(qcRejectionCode)); } catch(Exception e) { RSP = "Failed to delete Instance with UID = "+sopInstanceUID; return Response.status(Status.CONFLICT).entity(RSP).build(); } RSP+=sopInstanceUID; return Response.ok(RSP).build(); } private Code checkRejectionCode(Code qcRejectionCode) { return qcRejectionCode == null ? new Code("113037", "DCM", null, "Rejected for Patient Safety Reasons") : qcRejectionCode; } /** * Delete series if empty. * * @param studyInstanceUID * the study instance uid * @param seriesInstanceUID * the series instance uid * @return the response */ @DELETE @Path("purge/studies/{StudyInstanceUID}/series/{SeriesInstanceUID}") public Response deleteSeriesIfEmpty( @PathParam("StudyInstanceUID") String studyInstanceUID, @PathParam("SeriesInstanceUID") String seriesInstanceUID) { RSP = "Series with UID = " + seriesInstanceUID + " was empty and Deleted = " + scService.deleteSeriesIfEmpty(seriesInstanceUID, studyInstanceUID); return Response.ok(RSP).build(); } /** * Delete study if empty. * * @param studyInstanceUID * the study instance uid * @return the response */ @DELETE @Path("purge/studies/{StudyInstanceUID}") public Response deleteStudyIfEmpty( @PathParam("StudyInstanceUID") String studyInstanceUID) { RSP = "Study with UID = " + studyInstanceUID + " was empty and Deleted = " + scService.deleteStudyIfEmpty(studyInstanceUID); return Response.ok(RSP).build(); } /** * Delete patient if empty. * * @param patientid * the patient id * @param issuer * the issue string separated by colons * @return the response */ @DELETE @Path("purge/patient/{PatientID}/issuer/{IssuerID}") public Response deletePatientIfEmpty( @PathParam("PatientID") String patientID, @PathParam("IssuerID") String issuerID) { IDWithIssuer pid = new IDWithIssuer(patientID, new Issuer(issuerID, ':')); RSP = "Patient with PID = " + pid.toString() + " was empty and Deleted = " + scService.deletePatientIfEmpty(pid); return Response.ok(RSP).build(); } /** * Aggregate patient operation response. * * @param attrs * the attributes used for each patient operation * @param applicationEntity * the application entity * @param command * the command * @param patientOperation * the patient operation * @return the response * @throws QCOperationNotPermittedException */ private Response aggregatePatientOpResponse(ArrayList<Attributes> attrs, ApplicationEntity applicationEntity, PatientCommands command, String patientOperation) throws QCOperationNotPermittedException { ArrayList<Boolean> listRSP = new ArrayList<Boolean>(); for (int i = 0; i < attrs.size(); i++) { listRSP.add(scService.patientOperation(attrs.get(i), attrs.get(++i), arcAEExt, command)); } int trueCount=0; for (boolean rsp : listRSP) { if (rsp) { trueCount++; } } return trueCount==listRSP.size()? Response.status(Status.OK).entity ("Patient operation successful - "+patientOperation).build(): trueCount==0?Response.status(Status.CONFLICT).entity ("Error - Unable to perform patient operation - "+patientOperation).build(): Response.status(Status.CONFLICT).entity ("Warning - Unable to perform some operations - "+patientOperation).build(); } /** * Parses the JSON attributes to list of attributes. * Used by the patient operations. * * @param in * the input stream * @return the array list * @throws IOException * Signals that an I/O exception has occurred. */ private ArrayList<Attributes> parseJSONAttributesToList(InputStream in) throws IOException { JSONReader reader = new JSONReader( Json.createParser(new InputStreamReader(in, "UTF-8"))); final ArrayList<Attributes> attributesList = new ArrayList<Attributes>(); reader.readDatasets(new Callback() { @Override public void onDataset(Attributes fmi, Attributes dataset) { attributesList.add(dataset); } }); ElementDictionary dict = ElementDictionary .getStandardElementDictionary(); for (int i = 0; i < attributesList.size(); i++){ HashMap<String, String> tmpQMap = new HashMap<String, String>(); for (int j = 0; j < attributesList.get(i).tags().length; j++) { Attributes ds = attributesList.get(i); if (TagUtils.isPrivateTag(ds.tags()[j])) { dict = ElementDictionary.getElementDictionary(ds .getPrivateCreator(ds.tags()[j])); } else { dict = ElementDictionary.getStandardElementDictionary(); tmpQMap.put(dict.keywordOf(ds.tags()[j]), ds.getString(ds.tags()[j])); } } } if (attributesList.size()%2!=0) throw new WebApplicationException(new Exception( "Unable to decide request data"), Response.Status.BAD_REQUEST); return attributesList; } /** * Initialize code. * * @param object * the QCobject with a rejection code * @return the rejection code */ private Code initializeCode(QCObject object) { return new Code(object.getQcRejectionCode().getCodeValue(), object.getQcRejectionCode().getCodingSchemeDesignator(), object.getQcRejectionCode().getCodingSchemeVersion(), object.getQcRejectionCode().getCodeMeaning()); } /** * Initialize id with issuer. * * @param object * the qc object * @return the ID with issuer */ private IDWithIssuer initializeIDWithIssuer(QCObject object) { IssuerObject issuerObj = object.getPid().getIssuer(); return new IDWithIssuer(object.getPid().getId(), new Issuer(issuerObj.getLocalNamespaceEntityID(), issuerObj.getUniversalEntityID(), issuerObj.getUniversalEntityIDType())); } }