/******************************************************************************* * Copyright (c) 2016 Weasis Team and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nicolas Roduit - initial API and implementation *******************************************************************************/ package org.weasis.acquire.explorer; import java.awt.Component; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.dcm4che3.data.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.weasis.acquire.explorer.core.bean.Global; import org.weasis.acquire.explorer.core.bean.SeriesGroup; import org.weasis.core.api.command.Option; import org.weasis.core.api.command.Options; import org.weasis.core.api.explorer.ObservableEvent; import org.weasis.core.api.gui.util.GuiExecutor; import org.weasis.core.api.media.MimeInspector; import org.weasis.core.api.media.data.ImageElement; import org.weasis.core.api.media.data.MediaElement; import org.weasis.core.api.media.data.TagW; import org.weasis.core.api.util.GzipManager; import org.weasis.core.api.util.NetworkUtil; import org.weasis.core.api.util.StringUtil; import org.weasis.core.ui.docking.UIManager; import org.weasis.core.ui.editor.image.ViewCanvas; import org.weasis.dicom.codec.TagD; import org.weasis.dicom.codec.TagD.Level; import org.xml.sax.SAXException; /** * * @author Yannick LARVOR * @version 2.5.0 * @since 2.5.0 - 2016-04-13 - ylar - Creation * */ public class AcquireManager { private static final Logger LOGGER = LoggerFactory.getLogger(AcquireManager.class); public static final String[] functions = { "patient" }; //$NON-NLS-1$ public static final Global GLOBAL = new Global(); private static final int OPT_NONE = 0; private static final int OPT_B64 = 1; private static final int OPT_ZIP = 2; private static final int OPT_URLSAFE = 4; private static final int OPT_B64ZIP = 3; private static final int OPT_B64URLSAFE = 5; private static final int OPT_B64URLSAFEZIP = 7; private static final AcquireManager instance = new AcquireManager(); private static final Map<String, AcquireImageInfo> imagesInfoByUID = new HashMap<>(); private static final Map<URI, AcquireImageInfo> imagesInfoByURI = new HashMap<>(); private AcquireImageInfo currentAcquireImageInfo = null; private ViewCanvas<ImageElement> currentView = null; private PropertyChangeSupport propertyChange = null; private AcquireExplorer acquireExplorer = null; private AcquireManager() { } public static AcquireManager getInstance() { return instance; } public static AcquireImageInfo getCurrentAcquireImageInfo() { return getInstance().currentAcquireImageInfo; } public static void setCurrentAcquireImageInfo(AcquireImageInfo imageInfo) { getInstance().currentAcquireImageInfo = imageInfo; } public static ViewCanvas<ImageElement> getCurrentView() { return getInstance().currentView; } public static void setCurrentView(ViewCanvas<ImageElement> view) { getInstance().currentView = view; // Remove capabilities to open a view by dragging a thumbnail from the import panel. view.getJComponent().setTransferHandler(null); } public static Collection<AcquireImageInfo> getAllAcquireImageInfo() { return imagesInfoByURI.values(); } public static AcquireImageInfo findByUId(String uid) { return imagesInfoByUID.get(uid); } public static AcquireImageInfo findByImage(ImageElement image) { return getInstance().getAcquireImageInfo(image); } public static List<AcquireImageInfo> findbySeries(SeriesGroup seriesGroup) { return getAcquireImageInfoList().stream() .filter(i -> i.getSeries() != null && i.getSeries().equals(seriesGroup)).collect(Collectors.toList()); } public static List<SeriesGroup> getBySeries() { return imagesInfoByURI.entrySet().stream().map(e -> e.getValue().getSeries()).filter(Objects::nonNull) .distinct().sorted().collect(Collectors.toList()); } public static Map<SeriesGroup, List<AcquireImageInfo>> groupBySeries() { return getAcquireImageInfoList().stream().filter(e -> e.getSeries() != null) .collect(Collectors.groupingBy(AcquireImageInfo::getSeries)); } public static SeriesGroup getSeries(SeriesGroup searched) { return getBySeries().stream().filter(s -> s.equals(searched)).findFirst().orElse(searched); } public static SeriesGroup getDefaultSeries() { return getBySeries().stream().filter(s -> SeriesGroup.Type.NONE.equals(s.getType())).findFirst() .orElseGet(SeriesGroup::new); } public void removeMedias(List<? extends MediaElement> mediaList) { removeImages(toAcquireImageInfo(mediaList)); } public void removeAllImages() { imagesInfoByURI.clear(); imagesInfoByUID.clear(); notifyPatientContextChanged(); } public void removeImages(Collection<AcquireImageInfo> imageCollection) { imageCollection.stream().filter(Objects::nonNull).forEach(this::removeImageFromDataMapping); notifyImagesRemoved(imageCollection); } public void removeImage(AcquireImageInfo imageElement) { Optional.ofNullable(imageElement).ifPresent(this::removeImageFromDataMapping); notifyImageRemoved(imageElement); } public void addImages(Collection<AcquireImageInfo> imageInfoCollection) { imageInfoCollection.stream().filter(Objects::nonNull).forEach(this::addImageToDataMapping); notifyImagesAdded(imageInfoCollection); } public void addImage(AcquireImageInfo imageInfo) { Optional.ofNullable(imageInfo).ifPresent(this::addImageToDataMapping); notifyImageAdded(imageInfo); } private static boolean isImageInfoPresent(AcquireImageInfo imageInfo) { return Optional.of(imageInfo).map(AcquireImageInfo::getImage).map(ImageElement::getMediaURI) .map(imagesInfoByURI::get).isPresent(); } public static void importImages(Collection<AcquireImageInfo> toImport, SeriesGroup searchedSeries, int maxRangeInMinutes) { Objects.requireNonNull(searchedSeries); boolean isSearchSeriesByDate = SeriesGroup.Type.DATE.equals(searchedSeries.getType()); SeriesGroup commonSeries = isSearchSeriesByDate ? null : getSeries(searchedSeries); List<AcquireImageInfo> imageImportedList = new ArrayList<>(toImport.size()); for (AcquireImageInfo newImageInfo : toImport) { if (isImageInfoPresent(newImageInfo)) { getInstance().getAcquireExplorer().getCentralPane().tabbedPane.removeImage(newImageInfo); }else { getInstance().addImageToDataMapping(newImageInfo); } newImageInfo.setSeries( isSearchSeriesByDate ? findSeries(searchedSeries, newImageInfo, maxRangeInMinutes) : commonSeries); if (isSearchSeriesByDate) { List<AcquireImageInfo> imageInfoList = AcquireManager.findbySeries(newImageInfo.getSeries()); if (imageInfoList.size() > 2) { recalculateCentralTime(imageInfoList); } } imageImportedList.add(newImageInfo); } getInstance().notifyImagesAdded(imageImportedList); } public static void importImage(AcquireImageInfo newImageInfo, SeriesGroup searchedSeries, int maxRangeInMinutes) { Objects.requireNonNull(searchedSeries); Objects.requireNonNull(newImageInfo); if (isImageInfoPresent(newImageInfo)) { getInstance().getAcquireExplorer().getCentralPane().tabbedPane.removeImage(newImageInfo); }else { getInstance().addImageToDataMapping(newImageInfo); } newImageInfo.setSeries(searchedSeries); getInstance().notifyImageAdded(newImageInfo); } private static SeriesGroup findSeries(SeriesGroup searchedSeries, AcquireImageInfo imageInfo, int maxRangeInMinutes) { Objects.requireNonNull(imageInfo, "findSeries imageInfo should not be null"); //$NON-NLS-1$ if (SeriesGroup.Type.DATE.equals(searchedSeries.getType())) { LocalDateTime imageDate = TagD.dateTime(Tag.ContentDate, Tag.ContentTime, imageInfo.getImage()); Optional<SeriesGroup> series = getBySeries().stream().filter(s -> SeriesGroup.Type.DATE.equals(s.getType())).filter(s -> { LocalDateTime start = s.getDate(); LocalDateTime end = imageDate; if (end.isBefore(start)) { start = imageDate; end = s.getDate(); } Duration duration = Duration.between(start, end); return duration.toMinutes() < maxRangeInMinutes; }).findFirst(); return series.isPresent() ? series.get() : getSeries(new SeriesGroup(imageDate)); } else { return getSeries(searchedSeries); } } private static void recalculateCentralTime(List<AcquireImageInfo> imageInfoList) { Objects.requireNonNull(imageInfoList); List<AcquireImageInfo> sortedList = imageInfoList.stream() .sorted(Comparator.comparing(i -> TagD.dateTime(Tag.ContentDate, Tag.ContentTime, i.getImage()))) .collect(Collectors.toList()); AcquireImageInfo info = sortedList.get(sortedList.size() / 2); info.getSeries().setDate(TagD.dateTime(Tag.ContentDate, Tag.ContentTime, info.getImage())); } public static List<ImageElement> toImageElement(List<? extends MediaElement> medias) { return medias.stream().filter(ImageElement.class::isInstance).map(ImageElement.class::cast) .collect(Collectors.toList()); } public static List<AcquireImageInfo> toAcquireImageInfo(List<? extends MediaElement> medias) { return medias.stream().filter(ImageElement.class::isInstance).map(ImageElement.class::cast).map(AcquireManager::findByImage) .collect(Collectors.toList()); } public static String getPatientContextName() { String patientName = TagD.getDicomPersonName(TagD.getTagValue(AcquireManager.GLOBAL, Tag.PatientName, String.class)); if (!org.weasis.core.api.util.StringUtil.hasLength(patientName)) { patientName = TagW.NO_VALUE; } return patientName; } public void registerDataExplorerView(AcquireExplorer explorer) { unRegisterDataExplorerView(); if (Objects.nonNull(explorer)) { this.acquireExplorer = explorer; this.addPropertyChangeListener(explorer); } } public void unRegisterDataExplorerView() { if (Objects.nonNull(this.acquireExplorer)) { this.removePropertyChangeListener(acquireExplorer); } } private void addPropertyChangeListener(PropertyChangeListener propertychangelistener) { if (propertyChange == null) { propertyChange = new PropertyChangeSupport(this); } propertyChange.addPropertyChangeListener(propertychangelistener); } private void removePropertyChangeListener(PropertyChangeListener propertychangelistener) { if (propertyChange != null) { propertyChange.removePropertyChangeListener(propertychangelistener); } } private void firePropertyChange(final ObservableEvent event) { if (propertyChange != null) { if (event == null) { throw new NullPointerException(); } if (SwingUtilities.isEventDispatchThread()) { propertyChange.firePropertyChange(event); } else { SwingUtilities.invokeLater(() -> propertyChange.firePropertyChange(event)); } } } private void notifyImageRemoved(AcquireImageInfo imageElement) { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, AcquireManager.this, null, imageElement)); } private void notifyImagesRemoved(Collection<AcquireImageInfo> imageCollection) { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.REMOVE, AcquireManager.this, null, imageCollection)); } private void notifyImageAdded(AcquireImageInfo imageInfo) { firePropertyChange(new ObservableEvent(ObservableEvent.BasicAction.ADD, AcquireManager.this, null, imageInfo)); } private void notifyImagesAdded(Collection<AcquireImageInfo> imageInfoCollection) { firePropertyChange( new ObservableEvent(ObservableEvent.BasicAction.ADD, AcquireManager.this, null, imageInfoCollection)); } private void notifyPatientContextChanged() { firePropertyChange(new ObservableEvent(ObservableEvent.BasicAction.REPLACE, AcquireManager.this, null, getPatientContextName())); } private void notifyPatientContextUpdated() { firePropertyChange(new ObservableEvent(ObservableEvent.BasicAction.UPDATE, AcquireManager.this, null, null)); } /** * Set a new Patient Context and in case current state job is not finished ask user if cleaning unpublished images * should be done or canceled. * * @param argv * @throws IOException */ public void patient(String[] argv) throws IOException { final String[] usage = { "Load Patient Context from the first argument", "Usage: acquire:patient (-x | -i | -s | -u) arg", //$NON-NLS-1$ //$NON-NLS-2$ "arg is an XML text in UTF8 or an url with the option '--url'", //$NON-NLS-1$ " -x --xml Open Patient Context from an XML data containing all DICOM Tags ", //$NON-NLS-1$ " -i --inbound Open Patient Context from an XML data containing all DICOM Tags, decoding syntax is [Base64/GZip]", //$NON-NLS-1$ " -s --iurlsafe Open Patient Context from an XML data containing all DICOM Tags, decoding syntax is [Base64_URL_SAFE/GZip]", //$NON-NLS-1$ " -u --url Open Patient Context from an URL (XML file containing all DICOM TAGs)", //$NON-NLS-1$ " -? --help show help" }; //$NON-NLS-1$ final Option opt = Options.compile(usage).parse(argv); final List<String> args = opt.args(); if (opt.isSet("help") || args.isEmpty()) { //$NON-NLS-1$ opt.usage(); return; } GuiExecutor.instance().execute(() -> patientCommand(opt, args.get(0))); } private void patientCommand(Option opt, String arg) { final Document newPatientContext; if (opt.isSet("xml")) { //$NON-NLS-1$ newPatientContext = getPatientContext(arg, OPT_NONE); } else if (opt.isSet("inbound")) { //$NON-NLS-1$ newPatientContext = getPatientContext(arg, OPT_B64ZIP); } else if (opt.isSet("iurlsafe")) { //$NON-NLS-1$ newPatientContext = getPatientContext(arg, OPT_B64URLSAFEZIP); } else if (opt.isSet("url")) { //$NON-NLS-1$ newPatientContext = getPatientContextFromUrl(arg); } else { newPatientContext = null; } if (newPatientContext != null) { if (!isPatientContextIdentical(newPatientContext)) { if (!isAcquireImagesAllPublished()) { if (JOptionPane.showConfirmDialog(getExplorerViewComponent(), Messages.getString("AcquireManager.new_patient_load_warn"), //$NON-NLS-1$ Messages.getString("AcquireManager.new_patient_load_title"), //$NON-NLS-1$ JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) != JOptionPane.OK_OPTION) { return; } } imagesInfoByURI.clear(); imagesInfoByUID.clear(); GLOBAL.init(newPatientContext); notifyPatientContextChanged(); } else { GLOBAL.updateAllButPatient(newPatientContext); getBySeries().stream().forEach(SeriesGroup::updateDicomTags); notifyPatientContextUpdated(); } } } public Component getExplorerViewComponent() { return Optional.ofNullable(acquireExplorer).map(AcquireExplorer::getCentralPane).map(Component.class::cast) .orElse(UIManager.getApplicationWindow()); } public AcquireExplorer getAcquireExplorer() { return acquireExplorer; } /** * Evaluates if patientContext currently loaded is identical to the one that's expected to be loaded according to * the Dicom Patient Group Only * * @return */ private static boolean isPatientContextIdentical(Document newPatientContext) { return GLOBAL.containsSamePatientTagValues(newPatientContext); } /** * Evaluate if all imported acquired images habe been published without any work in progress state. * * @return */ private boolean isAcquireImagesAllPublished() { return getAllAcquireImageInfo().stream().allMatch(i -> i.getStatus() == AcquireImageStatus.PUBLISHED); } /** * * @param inputString * @param codeOption * @return */ private static Document getPatientContext(String inputString, int codeOption) { return getPatientContext(inputString.getBytes(StandardCharsets.UTF_8), codeOption); } /** * * @param byteArray * @param codeOption * @return */ private static Document getPatientContext(byte[] byteArray, int codeOption) { if (byteArray == null || byteArray.length == 0) { throw new IllegalArgumentException("empty byteArray parameter"); //$NON-NLS-1$ } if (codeOption != OPT_NONE) { try { if ((codeOption & OPT_B64) == OPT_B64) { if ((codeOption & OPT_URLSAFE) == OPT_URLSAFE) { byteArray = Base64.getUrlDecoder().decode(byteArray); } else { byteArray = Base64.getDecoder().decode(byteArray); } } if ((codeOption & OPT_ZIP) == OPT_ZIP) { byteArray = GzipManager.gzipUncompressToByte(byteArray); } } catch (Exception e) { LOGGER.error("Decode Patient Context", e); //$NON-NLS-1$ return null; } } try (InputStream inputStream = new ByteArrayInputStream(byteArray)) { LOGGER.debug("Source XML :\n{}", new String(byteArray)); //$NON-NLS-1$ return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream); } catch (SAXException | IOException | ParserConfigurationException e) { LOGGER.error("Parsing Patient Context XML", e); //$NON-NLS-1$ } return null; } /** * * @param uri * @return */ private static Document getPatientContextFromUri(URI uri) { byte[] byteArray = getURIContent(Objects.requireNonNull(uri)); String uriPath = uri.getPath(); if (uriPath.endsWith(".gz") || !(uriPath.endsWith(".xml") //$NON-NLS-1$ //$NON-NLS-2$ && MimeInspector.isMatchingMimeTypeFromMagicNumber(byteArray, "application/x-gzip"))) { //$NON-NLS-1$ return getPatientContext(byteArray, OPT_ZIP); } else { return getPatientContext(byteArray, OPT_NONE); } } /** * * @param url * @return */ private static Document getPatientContextFromUrl(String url) { return getPatientContextFromUri(getURIFromURL(url)); } /** * * @param urlStr * @return */ private static URI getURIFromURL(String urlStr) { if (!StringUtil.hasText(urlStr)) { throw new IllegalArgumentException("empty urlString parameter"); //$NON-NLS-1$ } URI uri = null; if (!urlStr.startsWith("http")) { //$NON-NLS-1$ try { File file = new File(urlStr); if (file.canRead()) { uri = file.toURI(); } } catch (Exception e) { LOGGER.error("{} is supposed to be a file URL but cannot be converted to a valid URI", urlStr, e); //$NON-NLS-1$ } } if (uri == null) { try { uri = new URL(urlStr).toURI(); } catch (MalformedURLException | URISyntaxException e) { LOGGER.error("getURIFromURL : {}", urlStr, e); //$NON-NLS-1$ } } return uri; } /** * * @param uri * @return */ private static byte[] getURIContent(URI uri) { try { URL url = Objects.requireNonNull(uri).toURL(); LOGGER.debug("Download from URL: {}", url); //$NON-NLS-1$ // note: fastest way to convert inputStream to string according to : // http://stackoverflow.com/questions/309424/read-convert-an-inputstream-to-a-string try (InputStream inputStream = NetworkUtil.getUrlInputStream(url.openConnection())) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, length); } return outputStream.toByteArray(); } } catch (Exception e) { LOGGER.error("Downloading URI content", e); //$NON-NLS-1$ } return null; } private void addImageToDataMapping(AcquireImageInfo imageInfo) { Objects.requireNonNull(imageInfo); imagesInfoByURI.put(imageInfo.getImage().getMediaURI(), imageInfo); imagesInfoByUID.put((String) imageInfo.getImage().getTagValue(TagD.getUID(Level.INSTANCE)), imageInfo); } private void removeImageFromDataMapping(AcquireImageInfo imageInfo) { imagesInfoByURI.remove(imageInfo.getImage().getMediaURI()); imagesInfoByUID.remove(imageInfo.getImage().getTagValue(TagD.getUID(Level.INSTANCE))); } private static List<AcquireImageInfo> getAcquireImageInfoList() { return imagesInfoByURI.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList()); } /** * Get AcquireImageInfo from the data model and create lazily the JAI.PlanarImage if not yet available<br> * * @note All the AcquireImageInfo value objects are unique according to the imageElement URI * * @param image * @return */ // TODO be carefull not to execute this method on the EDT private AcquireImageInfo getAcquireImageInfo(ImageElement image) { if (image == null || image.getImage() == null) { return null; } AcquireImageInfo imageInfo = imagesInfoByURI.get(image.getMediaURI()); if (imageInfo == null) { imageInfo = new AcquireImageInfo(image); } return imageInfo; } }