├── .gitignore ├── CODEOWNERS ├── google393c719b8636389e.html ├── google_dicom_explorer.png ├── src ├── main │ ├── resources │ │ ├── loading.gif │ │ └── client_secrets.json │ └── java │ │ └── org │ │ └── weasis │ │ └── dicom │ │ └── google │ │ ├── explorer │ │ ├── messages.properties │ │ ├── internal │ │ │ └── Activator.java │ │ ├── Messages.java │ │ ├── GoogleDicomExplorerFactory.java │ │ ├── GoogleDicomExplorer.java │ │ └── DownloadManager.java │ │ └── api │ │ ├── ui │ │ ├── dicomstore │ │ │ ├── StoreUpdateListener.java │ │ │ ├── StoreUpdateEvent.java │ │ │ ├── LoadDatasetsTask.java │ │ │ ├── LoadDicomStoresTask.java │ │ │ ├── LoadLocationsTask.java │ │ │ ├── LoadProjectsTask.java │ │ │ ├── AbstractDicomSelectorTask.java │ │ │ ├── AutoRefreshComboBoxExtension.java │ │ │ ├── GoogleLoginTask.java │ │ │ ├── LoadStudiesTask.java │ │ │ └── DicomStoreSelector.java │ │ ├── NavigationPanel.java │ │ ├── GoogleExplorer.java │ │ ├── StudyView.java │ │ ├── OAuth2Browser.java │ │ ├── StudiesTable.java │ │ └── SearchPanel.java │ │ ├── GoogleAPIClientFactory.java │ │ ├── model │ │ ├── ProjectDescriptor.java │ │ ├── Dataset.java │ │ ├── DicomStore.java │ │ ├── Location.java │ │ ├── StudyQuery.java │ │ └── StudyModel.java │ │ ├── util │ │ └── StringUtils.java │ │ └── GoogleAPIClient.java └── test │ └── java │ └── org │ └── weasis │ └── dicom │ └── google │ └── api │ └── GoogleAPIClientTest.java ├── cloudbuild.yaml ├── release ├── githubRelease.yaml └── githubRelease.sh ├── CONTRIBUTING.md ├── README.md ├── pom.xml └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | /.settings/ 3 | /.classpath 4 | /.project 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @poojavenkatram 2 | * @danielbeaudreau 3 | * @sbabitz 4 | -------------------------------------------------------------------------------- /google393c719b8636389e.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google393c719b8636389e.html -------------------------------------------------------------------------------- /google_dicom_explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/weasis-chcapi-extension/HEAD/google_dicom_explorer.png -------------------------------------------------------------------------------- /src/main/resources/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleCloudPlatform/weasis-chcapi-extension/HEAD/src/main/resources/loading.gif -------------------------------------------------------------------------------- /src/main/resources/client_secrets.json: -------------------------------------------------------------------------------- 1 | {"installed":{"client_id":"952621265781-q7lsqhhths8jp5k124nqj7qo1la92ps5.apps.googleusercontent.com","project_id":"chc-github-weasis-ext","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"NlHxVP2X3OimN4DS4o09OxGz","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: "gcr.io/cloud-builders/git" 3 | args: 4 | - "clone" 5 | - "https://github.com/nroduit/Weasis.git" 6 | - "weasis" 7 | - name: "gcr.io/cloud-builders/git" 8 | args: 9 | - "checkout" 10 | - "tags/v3.6.0" 11 | dir: "weasis" 12 | - name: "maven:3.6.3-jdk-14" 13 | volumes: 14 | - name: "mavenLocal" 15 | path: "/root/.m2" 16 | args: 17 | - "mvn" 18 | - "install" 19 | dir: "weasis" 20 | - name: "maven:3.6.3-jdk-14" 21 | volumes: 22 | - name: "mavenLocal" 23 | path: "/root/.m2" 24 | args: 25 | - "mvn" 26 | - "install" 27 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/messages.properties: -------------------------------------------------------------------------------- 1 | 2 | GoogleDicomExplorer.title=GOOGLEDICOM 3 | GoogleDicomExplorer.btn_title=Google Healthcare Explorer 4 | GoogleDicomExplorer.desc=Explore Google Healthcare Dicom data 5 | DicomStoreSelector.sign_in=Google Sign In 6 | DicomStoreSelector.sign_out=Google Sign Out 7 | DicomStoreSelector.default_project_text=-- Choose or type project -- 8 | DicomStoreSelector.default_location_text=-- Choose location -- 9 | DicomStoreSelector.default_dataset_text=-- Choose dataset -- 10 | DicomStoreSelector.default_dicomstore_text=-- Choose store -- 11 | GoogleAPIClient.open_browser_message=The system cannot open your default browser to authorize.\nAuthorization URL has been copied to clipboard.\nPlease paste it in your browser and follow instructions. 12 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/StoreUpdateListener.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import java.util.EventListener; 18 | /** Listener for DICOM store updates 19 | */ 20 | public interface StoreUpdateListener extends EventListener { 21 | void actionPerformed(StoreUpdateEvent event); 22 | } -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/internal/Activator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.explorer.internal; 16 | 17 | import org.osgi.framework.BundleActivator; 18 | import org.osgi.framework.BundleContext; 19 | 20 | public class Activator implements BundleActivator { 21 | 22 | 23 | @Override 24 | public void start(final BundleContext context) { 25 | 26 | } 27 | 28 | @Override 29 | public void stop(BundleContext context) { 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /release/githubRelease.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: "gcr.io/cloud-builders/git" 3 | args: 4 | - "clone" 5 | - "https://github.com/nroduit/Weasis.git" 6 | - "weasis" 7 | - name: "gcr.io/cloud-builders/git" 8 | args: 9 | - "checkout" 10 | - "tags/v3.6.0" 11 | dir: "weasis" 12 | - name: "maven:3.6.3-jdk-14" 13 | volumes: 14 | - name: "mavenLocal" 15 | path: "/root/.m2" 16 | args: 17 | - "mvn" 18 | - "install" 19 | dir: "weasis" 20 | - name: "maven:3.6.3-jdk-14" 21 | volumes: 22 | - name: "mavenLocal" 23 | path: "/root/.m2" 24 | args: 25 | - "mvn" 26 | - "install" 27 | - name: 'google/cloud-sdk:290.0.1' 28 | args: 29 | - 'bash' 30 | - './release/githubRelease.sh' 31 | - '$REPO_NAME' 32 | - '$TAG_NAME' 33 | secretEnv: 34 | - 'ACCESS_TOKEN' 35 | timeout: 600s 36 | secrets: 37 | - kmsKeyName: projects/gcp-healthcare-oss-test/locations/global/keyRings/default/cryptoKeys/github-robot-access-token 38 | secretEnv: 39 | ACCESS_TOKEN: CiQAM/SK3FUc1t+CnHDdgRzbc556FIyHddxRpsnolmSKfpiZ66sSUQDrEGO9gz15JIulryNagWzUOGbBEAaC04y85J8fNRjJZ8T8ntzh6Kt0Sa+GCG+3n5xSQdDJdj6xOG0LfVzvU+/K3mZ1KJlIcd0jiCeBrjYLlw== 40 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/StoreUpdateEvent.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package org.weasis.dicom.google.api.ui.dicomstore; 15 | 16 | import java.awt.AWTEvent; 17 | 18 | /** DICOM store update event to notify listeners 19 | */ 20 | public class StoreUpdateEvent extends AWTEvent { 21 | static int id; 22 | 23 | /** Creates a StoreUpdateEvent with the source object and a unique id 24 | * @return Store update event. 25 | */ 26 | public StoreUpdateEvent(Object source) { 27 | super(source, id++); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows 28 | [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/GoogleAPIClientFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api; 16 | 17 | public class GoogleAPIClientFactory { 18 | 19 | private static GoogleAPIClientFactory instance; 20 | 21 | private GoogleAPIClient googleAPIClient; 22 | 23 | public static GoogleAPIClientFactory getInstance() { 24 | if (instance == null) { 25 | instance = new GoogleAPIClientFactory(); 26 | } 27 | return instance; 28 | } 29 | 30 | public GoogleAPIClient createGoogleClient() { 31 | if (googleAPIClient == null) { 32 | googleAPIClient = new GoogleAPIClient(); 33 | } 34 | return googleAPIClient; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/Messages.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.explorer; 16 | 17 | import java.util.MissingResourceException; 18 | import java.util.ResourceBundle; 19 | 20 | public class Messages { 21 | private static final String BUNDLE_NAME = "org.weasis.dicom.google.explorer.messages"; //$NON-NLS-1$ 22 | 23 | private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); 24 | 25 | private Messages() { 26 | } 27 | 28 | public static String getString(String key) { 29 | try { 30 | return RESOURCE_BUNDLE.getString(key); 31 | } catch (MissingResourceException e) { 32 | return '!' + key + '!'; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/NavigationPanel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package org.weasis.dicom.google.api.ui; 15 | 16 | import javax.swing.JButton; 17 | import javax.swing.JLabel; 18 | import javax.swing.JPanel; 19 | import java.awt.FlowLayout; 20 | 21 | /** Panel to combine and align navigation buttons relative to searchPanel 22 | */ 23 | public class NavigationPanel extends JPanel { 24 | private SearchPanel searchPanel; 25 | 26 | public NavigationPanel(SearchPanel searchPanel) { 27 | this.searchPanel = searchPanel; 28 | JButton previousButton = searchPanel.getPageNumberButtonPrevious(); 29 | JButton nextButton = searchPanel.getPageNumberButtonNext(); 30 | JLabel pageNumberLabel = searchPanel.getPageNumberLabel(); 31 | this.setLayout(new FlowLayout(FlowLayout.LEFT)); 32 | this.add(previousButton); 33 | this.add(pageNumberLabel); 34 | this.add(nextButton); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/ProjectDescriptor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import java.util.Objects; 18 | 19 | public class ProjectDescriptor { 20 | 21 | private final String name; 22 | private final String id; 23 | 24 | public ProjectDescriptor(String name, String id) { 25 | this.name = name; 26 | this.id = id; 27 | } 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public String getId() { 34 | return id; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return name; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | ProjectDescriptor that = (ProjectDescriptor) o; 47 | return Objects.equals(name, that.name) && 48 | Objects.equals(id, that.id); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(name, id); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/Dataset.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import java.util.Objects; 18 | 19 | public class Dataset { 20 | 21 | private Location parent; 22 | 23 | private final String name; 24 | 25 | public Dataset(Location parent, String name) { 26 | this.parent = parent; 27 | this.name = name; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public Location getParent() { 35 | return parent; 36 | } 37 | 38 | public ProjectDescriptor getProject() { 39 | return parent.getParent(); 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | Dataset dataset = (Dataset) o; 47 | return Objects.equals(parent, dataset.parent) && 48 | Objects.equals(name, dataset.name); 49 | } 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(parent, name); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/LoadDatasetsTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.Dataset; 19 | import org.weasis.dicom.google.api.model.Location; 20 | 21 | import java.util.Comparator; 22 | import java.util.List; 23 | 24 | public class LoadDatasetsTask extends AbstractDicomSelectorTask> { 25 | 26 | private final Location location; 27 | 28 | public LoadDatasetsTask(Location location, 29 | GoogleAPIClient api, 30 | DicomStoreSelector view) { 31 | super(api, view); 32 | this.location = location; 33 | } 34 | 35 | @Override 36 | protected List doInBackground() throws Exception { 37 | List locations = api.fetchDatasets(location); 38 | locations.sort(Comparator.comparing(Dataset::getName)); 39 | return locations; 40 | } 41 | 42 | @Override 43 | protected void onCompleted(List result) { 44 | view.updateDatasets(result); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/util/StringUtils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.util; 16 | 17 | import java.io.UnsupportedEncodingException; 18 | import java.net.URLEncoder; 19 | import java.util.Collection; 20 | 21 | public final class StringUtils { 22 | 23 | private StringUtils() { 24 | } 25 | 26 | public static boolean isNotBlank(String str) { 27 | return str != null 28 | && !str.trim().isEmpty(); 29 | } 30 | 31 | public static String urlEncode(String str) { 32 | try { 33 | return URLEncoder.encode(str, "UTF-8"); 34 | } catch (UnsupportedEncodingException ex) { 35 | throw new IllegalStateException("Error on encoding url " + str, ex); 36 | } 37 | } 38 | 39 | public static String join(Collection collection, String joinString) { 40 | StringBuilder builder = new StringBuilder(); 41 | for (String str : collection) { 42 | if (builder.length() > 0) { 43 | builder.append(joinString); 44 | } 45 | builder.append(str); 46 | } 47 | 48 | return builder.toString(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/LoadDicomStoresTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.Dataset; 19 | import org.weasis.dicom.google.api.model.DicomStore; 20 | 21 | import java.util.Comparator; 22 | import java.util.List; 23 | 24 | public class LoadDicomStoresTask extends AbstractDicomSelectorTask> { 25 | 26 | private final Dataset dataset; 27 | 28 | public LoadDicomStoresTask(Dataset dataset, 29 | GoogleAPIClient api, 30 | DicomStoreSelector view) { 31 | super(api, view); 32 | this.dataset = dataset; 33 | } 34 | 35 | @Override 36 | protected List doInBackground() throws Exception { 37 | List locations = api.fetchDicomstores(dataset); 38 | locations.sort(Comparator.comparing(DicomStore::getName)); 39 | return locations; 40 | } 41 | 42 | @Override 43 | protected void onCompleted(List result) { 44 | view.updateDicomStores(result); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/LoadLocationsTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.Location; 19 | import org.weasis.dicom.google.api.model.ProjectDescriptor; 20 | 21 | import java.util.Comparator; 22 | import java.util.List; 23 | 24 | public class LoadLocationsTask extends AbstractDicomSelectorTask> { 25 | 26 | private final ProjectDescriptor project; 27 | 28 | public LoadLocationsTask(ProjectDescriptor project, 29 | GoogleAPIClient api, 30 | DicomStoreSelector view) { 31 | super(api, view); 32 | this.project = project; 33 | } 34 | 35 | @Override 36 | protected List doInBackground() throws Exception { 37 | List locations = api.fetchLocations(project); 38 | locations.sort(Comparator.comparing(Location::getName)); 39 | return locations; 40 | } 41 | 42 | @Override 43 | protected void onCompleted(List result) { 44 | view.updateLocations(result); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/LoadProjectsTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.ProjectDescriptor; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.util.Comparator; 23 | import java.util.List; 24 | 25 | public class LoadProjectsTask extends AbstractDicomSelectorTask> { 26 | private static final Logger LOGGER = LoggerFactory.getLogger(LoadProjectsTask.class); 27 | 28 | public LoadProjectsTask(GoogleAPIClient api, DicomStoreSelector view) { 29 | super(api, view); 30 | } 31 | 32 | @Override 33 | protected List doInBackground() throws Exception { 34 | List projects = api.fetchProjects(); 35 | projects.sort(Comparator.comparing(ProjectDescriptor::getName)); 36 | return projects; 37 | } 38 | 39 | @Override 40 | protected void onCompleted(List result) { 41 | LOGGER.debug("Loaded projects list " + result); 42 | view.updateProjects(result); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/DicomStore.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import java.util.Objects; 18 | 19 | public class DicomStore { 20 | 21 | private final Dataset parent; 22 | 23 | private final String name; 24 | 25 | public DicomStore(Dataset parent, String name) { 26 | this.parent = parent; 27 | this.name = name; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public Dataset getParent() { 35 | return parent; 36 | } 37 | 38 | public ProjectDescriptor getProject() { 39 | return parent.getProject(); 40 | } 41 | 42 | public Location getLocation() { 43 | return parent.getParent(); 44 | } 45 | 46 | @Override 47 | public boolean equals(Object o) { 48 | if (this == o) return true; 49 | if (o == null || getClass() != o.getClass()) return false; 50 | DicomStore that = (DicomStore) o; 51 | return Objects.equals(parent, that.parent) && 52 | Objects.equals(name, that.name); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return Objects.hash(parent, name); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/Location.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import java.util.Objects; 18 | 19 | public class Location { 20 | 21 | private final ProjectDescriptor parent; 22 | 23 | private final String name; 24 | private final String id; 25 | 26 | public Location(ProjectDescriptor parent, String name, String id) { 27 | this.parent = parent; 28 | this.name = name; 29 | this.id = id; 30 | } 31 | 32 | public String getName() { 33 | return name; 34 | } 35 | 36 | public String getId() { 37 | return id; 38 | } 39 | 40 | public ProjectDescriptor getParent() { 41 | return parent; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return id; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object o) { 51 | if (this == o) return true; 52 | if (o == null || getClass() != o.getClass()) return false; 53 | Location location = (Location) o; 54 | return Objects.equals(parent, location.parent) && 55 | Objects.equals(name, location.name) && 56 | Objects.equals(id, location.id); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return Objects.hash(parent, name, id); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/AbstractDicomSelectorTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import javax.swing.SwingWorker; 22 | import javax.swing.JOptionPane; 23 | import java.util.concurrent.ExecutionException; 24 | 25 | public abstract class AbstractDicomSelectorTask extends SwingWorker { 26 | private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDicomSelectorTask.class); 27 | 28 | protected final GoogleAPIClient api; 29 | protected final DicomStoreSelector view; 30 | 31 | public AbstractDicomSelectorTask(GoogleAPIClient api, DicomStoreSelector view) { 32 | this.api = api; 33 | this.view = view; 34 | } 35 | 36 | @Override 37 | protected final void done() { 38 | try { 39 | T result = get(); 40 | onCompleted(result); 41 | } catch (ExecutionException ex) { 42 | LOGGER.error("Error on dicom task", ex.getCause()); 43 | JOptionPane.showMessageDialog(null, ex.getCause().getMessage(), "Error", JOptionPane.ERROR_MESSAGE); 44 | } catch (InterruptedException ex) { 45 | LOGGER.error("Interrupted", ex); 46 | } 47 | } 48 | 49 | protected abstract void onCompleted(T result); 50 | } 51 | -------------------------------------------------------------------------------- /release/githubRelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2019 Google LLC 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | readonly REPO_NAME="${1}" 18 | readonly TAG_NAME="${2}" 19 | readonly TOKEN="${ACCESS_TOKEN}" 20 | # Get GitHub user and GitHub repo from REPO_NAME 21 | IFS='_' read -ra array <<< "${REPO_NAME}" 22 | github_user="${array[1]}" 23 | github_repo="${array[2]}" 24 | if [[ -z "${github_user}" ]] 25 | then 26 | github_user="GoogleCloudPlatform" 27 | github_repo="${REPO_NAME}" 28 | fi 29 | # Create request.json with request parameters 30 | echo "{\"tag_name\": \"${TAG_NAME}\",\"name\": \"${TAG_NAME}\"}" > request.json 31 | # Create a request for creating a release on GitHub page 32 | readonly resp_file="response.json" 33 | response_code="$(curl -# -X POST \ 34 | -H "Authorization: Bearer ${TOKEN}" \ 35 | -H "Content-Type:application/json" \ 36 | -H "Accept:application/json" \ 37 | -w "%{http_code}" \ 38 | --data-binary "@/workspace/request.json" \ 39 | "https://api.github.com/repos/${github_user}/${github_repo}/releases" \ 40 | -o "${resp_file}")" 41 | # Check status code 42 | if [[ "${response_code}" != 201 ]]; then 43 | cat "${resp_file}" 44 | exit 1 45 | fi 46 | # Get release id from response.json 47 | release_id="$(grep -wm 1 "id" /workspace/response.json \ 48 | | grep -Eo "[[:digit:]]+")" 49 | # Get JAR version from pom.xml 50 | jar_version="$(grep -m 1 "" /workspace/pom.xml \ 51 | | grep -Eo "[[:digit:]]+.[[:digit:]]+.[[:digit:]]+")" 52 | jar_name="weasis-chcapi-extension-${jar_version}.jar" 53 | # Upload JAR to GitHub releases page 54 | response_code="$(curl -# -X POST -H "Authorization: token ${TOKEN}" \ 55 | -H "Content-Type:application/octet-stream" \ 56 | -w "%{http_code}" \ 57 | --data-binary "@/workspace/target/${jar_name}" \ 58 | "https://uploads.github.com/repos/${github_user}/${github_repo}/releases/${release_id}/assets?name=${jar_name}" \ 59 | -o "${resp_file}")" 60 | # Check status code 61 | if [[ "${response_code}" != 201 ]]; then 62 | cat "${resp_file}" 63 | exit 2 64 | fi 65 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/GoogleDicomExplorerFactory.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.explorer; 16 | 17 | import org.osgi.service.component.ComponentContext; 18 | import org.osgi.service.component.annotations.Activate; 19 | import org.osgi.service.component.annotations.Deactivate; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.weasis.core.api.explorer.DataExplorerView; 23 | import org.weasis.core.api.explorer.DataExplorerViewFactory; 24 | import org.weasis.core.api.explorer.ObservableEvent; 25 | import org.weasis.core.ui.editor.ViewerPluginBuilder; 26 | 27 | import java.util.Hashtable; 28 | 29 | @org.osgi.service.component.annotations.Component(service = DataExplorerViewFactory.class, immediate = false) 30 | public class GoogleDicomExplorerFactory implements DataExplorerViewFactory { 31 | 32 | private GoogleDicomExplorer explorer = null; 33 | private final Logger LOGGER = LoggerFactory.getLogger(GoogleDicomExplorerFactory.class); 34 | 35 | @Override 36 | public DataExplorerView createDataExplorerView(Hashtable properties) { 37 | if (explorer == null) { 38 | explorer = new GoogleDicomExplorer(); 39 | ViewerPluginBuilder.DefaultDataModel.firePropertyChange( 40 | new ObservableEvent(ObservableEvent.BasicAction.NULL_SELECTION, explorer, null, null)); 41 | } 42 | return explorer; 43 | } 44 | 45 | // ================================================================================ 46 | // OSGI service implementation 47 | // ================================================================================ 48 | 49 | @Activate 50 | protected void activate(ComponentContext context) { 51 | 52 | LOGGER.info("Activate the Google Healthcare DataExplorerView"); 53 | } 54 | 55 | @Deactivate 56 | protected void deactivate(ComponentContext context) { 57 | LOGGER.info("Deactivate the Google Healthcare DataExplorerView"); 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/AutoRefreshComboBoxExtension.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import javax.swing.JComboBox; 18 | import javax.swing.event.ListDataEvent; 19 | import javax.swing.event.ListDataListener; 20 | import java.awt.event.MouseAdapter; 21 | import java.awt.event.MouseEvent; 22 | import java.util.function.Supplier; 23 | 24 | public class AutoRefreshComboBoxExtension { 25 | 26 | private static final long TIME_TO_INVALIDATE_CACHE_MS = 10_000; 27 | 28 | private long lastUpdateTime = System.currentTimeMillis(); 29 | 30 | private AutoRefreshComboBoxExtension(JComboBox comboBox, Supplier reload) { 31 | addDataUpdateListener(comboBox); 32 | addDataReloadListener(comboBox, reload); 33 | } 34 | 35 | public static AutoRefreshComboBoxExtension wrap(JComboBox comboBox, Supplier reload) { 36 | return new AutoRefreshComboBoxExtension(comboBox, reload); 37 | } 38 | 39 | private void addDataReloadListener(JComboBox comboBox, Supplier reload) { 40 | comboBox.addMouseListener(new MouseAdapter() { 41 | @Override 42 | public void mousePressed(MouseEvent e) { 43 | if (isTimeoutPassed()) { 44 | if (Boolean.TRUE.equals(reload.get())) { 45 | lastUpdateTime = System.currentTimeMillis(); 46 | } 47 | } 48 | } 49 | }); 50 | } 51 | 52 | private void addDataUpdateListener(JComboBox comboBox) { 53 | comboBox.getModel().addListDataListener(new ListDataListener() { 54 | @Override 55 | public void intervalAdded(ListDataEvent e) { 56 | lastUpdateTime = System.currentTimeMillis(); 57 | } 58 | 59 | @Override 60 | public void intervalRemoved(ListDataEvent e) { 61 | lastUpdateTime = System.currentTimeMillis(); 62 | } 63 | 64 | @Override 65 | public void contentsChanged(ListDataEvent e) { 66 | lastUpdateTime = System.currentTimeMillis(); 67 | } 68 | }); 69 | } 70 | 71 | private boolean isTimeoutPassed() { 72 | return (System.currentTimeMillis() - lastUpdateTime) > TIME_TO_INVALIDATE_CACHE_MS; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/GoogleLoginTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package org.weasis.dicom.google.api.ui.dicomstore; 15 | 16 | import javax.swing.JButton; 17 | import javax.swing.JOptionPane; 18 | import javax.swing.SwingWorker; 19 | 20 | import org.weasis.core.api.gui.util.GuiExecutor; 21 | import org.weasis.dicom.google.api.GoogleAPIClient; 22 | import org.weasis.dicom.google.explorer.Messages; 23 | 24 | public class GoogleLoginTask extends SwingWorker { 25 | protected final GoogleAPIClient googleAPIClient; 26 | protected final JButton googleAuthButton; 27 | protected final DicomStoreSelector view; 28 | 29 | private static final String TEXT_GOOGLE_SIGN_IN = Messages.getString("DicomStoreSelector.sign_in"); //$NON-NLS-1$ 30 | private static final String TEXT_GOOGLE_SIGN_OUT = Messages.getString("DicomStoreSelector.sign_out"); //$NON-NLS-1$ 31 | private static final String ACTION_SIGN_OUT = Messages.getString("DicomStoreSelector.sign_out"); 32 | private static final String ACTION_SIGN_IN = Messages.getString("DicomStoreSelector.sign_in"); 33 | 34 | public GoogleLoginTask(GoogleAPIClient apiClient, JButton googleAuthButton, DicomStoreSelector view) { 35 | this.googleAPIClient = apiClient; 36 | this.googleAuthButton = googleAuthButton; 37 | this.view = view; 38 | } 39 | 40 | 41 | @Override 42 | protected Void doInBackground() { 43 | try { 44 | googleAPIClient.signIn(); 45 | googleAuthButton.setText(TEXT_GOOGLE_SIGN_OUT); 46 | googleAuthButton.setActionCommand(ACTION_SIGN_OUT); 47 | new LoadProjectsTask(googleAPIClient, view).execute(); 48 | } catch (Exception ex) { 49 | GuiExecutor.instance().invokeAndWait(() -> JOptionPane.showMessageDialog(null, 50 | "Error occured on fetching google API.\n" + 51 | "Make sure you created OAuth Client ID credential \n" + 52 | "in Google Cloud console at https://console.cloud.google.com/apis/credentials \n" + 53 | "and copied your client_secrets.json to Weasis root folder.\n" + 54 | "Error message:" + ex.getCause().getMessage())); 55 | 56 | googleAPIClient.signOut(); 57 | googleAuthButton.setText(TEXT_GOOGLE_SIGN_IN); 58 | googleAuthButton.setActionCommand(ACTION_SIGN_IN); 59 | } 60 | return null; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/GoogleExplorer.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.ui.dicomstore.DicomStoreSelector; 19 | import org.weasis.dicom.google.explorer.DownloadManager; 20 | 21 | import javax.swing.JPanel; 22 | import javax.swing.BoxLayout; 23 | import javax.swing.Box; 24 | 25 | import java.awt.BorderLayout; 26 | import java.awt.Component; 27 | 28 | import static javax.swing.BoxLayout.PAGE_AXIS; 29 | 30 | public class GoogleExplorer extends JPanel { 31 | 32 | private final StudiesTable table; 33 | private final GoogleAPIClient googleAPIClient; 34 | private final DicomStoreSelector storeSelector; 35 | 36 | private final SearchPanel searchPanel; 37 | private final NavigationPanel navigationPanel; 38 | 39 | public GoogleExplorer(GoogleAPIClient googleAPIClient) { 40 | this.googleAPIClient = googleAPIClient; 41 | 42 | BorderLayout layout = new BorderLayout(); 43 | 44 | layout.setHgap(15); 45 | setLayout(layout); 46 | 47 | table = new StudiesTable(this); 48 | storeSelector = new DicomStoreSelector(googleAPIClient, table); 49 | searchPanel = new SearchPanel(storeSelector); 50 | navigationPanel = new NavigationPanel(searchPanel); 51 | add(centralComponent(), BorderLayout.CENTER); 52 | add(searchPanel, BorderLayout.WEST); 53 | } 54 | 55 | public Component centralComponent() { 56 | JPanel panel = new JPanel(); 57 | BoxLayout layout = new BoxLayout(panel, PAGE_AXIS); 58 | panel.setLayout(layout); 59 | 60 | panel.add(storeSelector); 61 | panel.add(Box.createVerticalStrut(10)); 62 | panel.add(table); 63 | panel.add(navigationPanel); 64 | return panel; 65 | } 66 | 67 | public void fireStudySelected(String studyId) { 68 | storeSelector.getCurrentStore() 69 | .map(store -> GoogleAPIClient.getImageUrl(store, studyId)) 70 | .ifPresent(image -> { 71 | DownloadManager.getLoadingExecutor().submit( 72 | new DownloadManager.LoadGoogleDicom(image, null, googleAPIClient, new DownloadManager.DownloadListener() { 73 | @Override 74 | public void downloadFinished() { 75 | table.hideLoadIcon(studyId); 76 | } 77 | })); 78 | table.showLoadIcon(studyId); 79 | }); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/StudyQuery.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import java.time.LocalDate; 18 | 19 | public class StudyQuery { 20 | 21 | private LocalDate startDate; 22 | private LocalDate endDate; 23 | private String patientName; 24 | private String patientId; 25 | 26 | private String accessionNumber; 27 | private String physicianName; 28 | private int page; 29 | private int pageSize; 30 | private boolean fuzzyMatching; 31 | 32 | public String getPatientName() { 33 | return patientName; 34 | } 35 | 36 | public void setPatientName(String patientName) { 37 | this.patientName = patientName; 38 | } 39 | 40 | public String getPatientId() { 41 | return patientId; 42 | } 43 | 44 | public void setPatientId(String patientId) { 45 | this.patientId = patientId; 46 | } 47 | 48 | public LocalDate getStartDate() { 49 | return startDate; 50 | } 51 | 52 | public void setStartDate(LocalDate startDate) { 53 | this.startDate = startDate; 54 | } 55 | 56 | public LocalDate getEndDate() { 57 | return endDate; 58 | } 59 | 60 | public void setEndDate(LocalDate endDate) { 61 | this.endDate = endDate; 62 | } 63 | 64 | public String getAccessionNumber() { 65 | return accessionNumber; 66 | } 67 | 68 | public void setAccessionNumber(String accessionNumber) { 69 | this.accessionNumber = accessionNumber; 70 | } 71 | 72 | public String getPhysicianName() { 73 | return physicianName; 74 | } 75 | 76 | public void setPhysicianName(String physicianName) { 77 | this.physicianName = physicianName; 78 | } 79 | 80 | public void setPage(int offset) { 81 | this.page = offset; 82 | } 83 | 84 | public int getPage() { 85 | return this.page; 86 | } 87 | 88 | public int getPageSize() { 89 | return this.pageSize; 90 | } 91 | 92 | /** Set how many objects will be requested for each page 93 | * Please note it may be hard for UI to display too many objects 94 | */ 95 | public void setPageSize(int pageSize) { 96 | this.pageSize = pageSize; 97 | } 98 | 99 | public void setFuzzyMatching(boolean fuzzyMatching) { 100 | this.fuzzyMatching = fuzzyMatching; 101 | } 102 | 103 | public boolean getFuzzyMatching() { 104 | return fuzzyMatching; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > Weasis now supports DICOMweb. This makes this extension obsolete. This repository has been archived and will not receive further updates. For details on how to integrate Weasis with DICOMweb end-points from Google Cloud Healthcare, please check https://weasis.org/en/tutorials/dicomweb-config/#google-cloud-healthcare-api 3 | 4 | ## About Weasis Google DICOM Plugin 5 | 6 | Plugin enables Weasis Viewer users access to [Google Cloud Healthcare API](https://cloud.google.com/healthcare) DICOM data. 7 | It utilizes [DICOMweb REST API](https://cloud.google.com/healthcare/docs/how-tos/dicomweb) to interact with Google Cloud services. 8 | 9 | ### Features 10 | 11 | * Login using your Google account 12 | * Interactive exploration of Google Healthcare API Dicom stores 13 | * Download and display all kinds of DICOM data 14 | * Advanced study search capabilities 15 | 16 | ![Google Dicom Explorer](google_dicom_explorer.png) 17 | 18 | ### Running the plugin 19 | 20 | The plugin runs as an extension to the main Weasis application, so first you 21 | need to download the main Weasis application from https://nroduit.github.io/en/. 22 | 23 | ***Please note, latest supported release of Weasis is [3.6.0](https://github.com/nroduit/Weasis/releases/tag/v3.6.0)*** 24 | 25 | Then you need to have existing data in the Cloud Healthcare API and install the 26 | plugin to get up and running. Please see more detailed instructions below. 27 | 28 | #### Setting up Google Cloud Healthcare API: 29 | 30 | See https://cloud.google.com/healthcare/docs/ to get started. 31 | 32 | #### Installing plugin 33 | 34 | * Get the latest release JAR from this repositories releases tab. 35 | * Follow instructions at [installing 36 | plug-ins](https://nroduit.github.io/en/basics/customize/build-plugins/#install-plug-ins) 37 | to add this plugin to Weasis. 38 | * Run Weasis Viewer executable 39 | * Switch to **_Google Dicom Explorer_** tab and login using your Google Account 40 | > NOTE: If you face with some issues when using the plugin, you should remove .Weasis folder wich may cache previous or 41 | > incorrect settings for the plugin in Weasis. 42 | 43 | #### Using your own OAuth client 44 | 45 | The plugin comes with it's own OAuth Client ID for ease of installation, but you can substitute 46 | your own if required (e.g. your organization has OAuth policy restriction on external apps). To do 47 | this go to the [Google API Console](https://console.developers.google.com/) and create a set of 48 | OAuth 2.0 credentials using the type "Other" and make sure to whitelist the scopes 49 | `.../auth/cloud-healthcare` and `.../auth/cloudplatformprojects.readonly`. Then download the 50 | credentials files in JSON format, name the file `client_secrets.json` and move it to conf 51 | folder, next to the ext-config.properties file. 52 | 53 | ### Building plugin 54 | 55 | If you're just trying to run the tool, please see the instructions above. If you 56 | need to recompile the plugin for any reason here are the steps to do so. 57 | 58 | Weasis requires JDK14. 59 | Plugin depends on core Weasis API, that's why you have to clone, build and install core Weasis modules to 60 | your local Maven repository first 61 | For this purpose follow instructions at [building Weasis](https://nroduit.github.io/en/getting-started/building-weasis/). 62 | After Weasis artifacts installed to your local Maven repository plugin itself can be compiled 63 | Detailed build instruction can be found at 64 | [building Weasis plugins](https://nroduit.github.io/en/basics/customize/build-plugins/) 65 | Clone this repository and execute following script: 66 | ```bash 67 | cd weasis-chcapi-extension 68 | 69 | ## build plugin 70 | mvn clean install 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/StudyView.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui; 16 | 17 | import java.time.LocalDate; 18 | import java.time.LocalTime; 19 | 20 | public class StudyView { 21 | 22 | private String studyId; 23 | private String patientName; 24 | private String patientId; 25 | private String accountNumber; 26 | private String noi; 27 | private LocalDate studyDate; 28 | private LocalTime studyTime; 29 | private String type; 30 | private String description; 31 | private String refPhd; 32 | private String reqPhd; 33 | private String location; 34 | private LocalDate birthDate; 35 | 36 | public String getStudyId() { 37 | return studyId; 38 | } 39 | 40 | public void setStudyId(String studyId) { 41 | this.studyId = studyId; 42 | } 43 | 44 | public String getPatientName() { 45 | return patientName; 46 | } 47 | 48 | public void setPatientName(String patientName) { 49 | this.patientName = patientName; 50 | } 51 | 52 | public String getPatientId() { 53 | return patientId; 54 | } 55 | 56 | public void setPatientId(String patientId) { 57 | this.patientId = patientId; 58 | } 59 | 60 | public String getAccountNumber() { 61 | return accountNumber; 62 | } 63 | 64 | public void setAccountNumber(String accountNumber) { 65 | this.accountNumber = accountNumber; 66 | } 67 | 68 | public String getNoi() { 69 | return noi; 70 | } 71 | 72 | public void setNoi(String noi) { 73 | this.noi = noi; 74 | } 75 | 76 | public LocalDate getStudyDate() { 77 | return studyDate; 78 | } 79 | 80 | public void setStudyDate(LocalDate studyDate) { 81 | this.studyDate = studyDate; 82 | } 83 | 84 | public LocalTime getStudyTime() { 85 | return studyTime; 86 | } 87 | 88 | public void setStudyTime(LocalTime studyTime) { 89 | this.studyTime = studyTime; 90 | } 91 | 92 | public String getType() { 93 | return type; 94 | } 95 | 96 | public void setType(String type) { 97 | this.type = type; 98 | } 99 | 100 | public String getDescription() { 101 | return description; 102 | } 103 | 104 | public void setDescription(String description) { 105 | this.description = description; 106 | } 107 | 108 | public String getRefPhd() { 109 | return refPhd; 110 | } 111 | 112 | public void setRefPhd(String refPhd) { 113 | this.refPhd = refPhd; 114 | } 115 | 116 | public String getReqPhd() { 117 | return reqPhd; 118 | } 119 | 120 | public void setReqPhd(String reqPhd) { 121 | this.reqPhd = reqPhd; 122 | } 123 | 124 | public String getLocation() { 125 | return location; 126 | } 127 | 128 | public void setLocation(String location) { 129 | this.location = location; 130 | } 131 | 132 | public LocalDate getBirthDate() { 133 | return birthDate; 134 | } 135 | 136 | public void setBirthDate(LocalDate birthDate) { 137 | this.birthDate = birthDate; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/GoogleDicomExplorer.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.explorer; 16 | 17 | import bibliothek.gui.dock.common.CLocation; 18 | import bibliothek.gui.dock.common.mode.ExtendedMode; 19 | import org.weasis.core.api.explorer.DataExplorerView; 20 | import org.weasis.core.api.explorer.model.DataExplorerModel; 21 | import org.weasis.core.ui.docking.PluginTool; 22 | import org.weasis.core.ui.editor.SeriesViewerEvent; 23 | import org.weasis.core.ui.editor.SeriesViewerListener; 24 | import org.weasis.dicom.google.api.GoogleAPIClient; 25 | import org.weasis.dicom.google.api.GoogleAPIClientFactory; 26 | import org.weasis.dicom.google.api.ui.GoogleExplorer; 27 | 28 | import javax.swing.*; 29 | import java.awt.*; 30 | import java.beans.PropertyChangeEvent; 31 | import java.io.File; 32 | import java.util.List; 33 | 34 | public class GoogleDicomExplorer extends PluginTool implements DataExplorerView, SeriesViewerListener { 35 | 36 | public static final String NAME = Messages.getString("GoogleDicomExplorer.title"); //$NON-NLS-1$ 37 | public static final String BUTTON_NAME = Messages.getString("GoogleDicomExplorer.btn_title"); //$NON-NLS-1$ 38 | public static final String DESCRIPTION = Messages.getString("GoogleDicomExplorer.desc"); //$NON-NLS-1$ 39 | 40 | private final GoogleExplorer explorer; 41 | 42 | private final GoogleAPIClient googleAPIClient = GoogleAPIClientFactory.getInstance().createGoogleClient(); 43 | 44 | public GoogleDicomExplorer() { 45 | super(NAME, BUTTON_NAME, POSITION.WEST, null,//ExtendedMode.NORMALIZED, 46 | PluginTool.Type.EXPLORER, 120); 47 | setLayout(new BorderLayout()); 48 | 49 | explorer = new GoogleExplorer(googleAPIClient); 50 | add(explorer); 51 | setDockableWidth(500); 52 | dockable.setMaximizable(true); 53 | dockable.setMinimizable(true); 54 | } 55 | 56 | 57 | @Override 58 | public void dispose() { 59 | super.closeDockable(); 60 | } 61 | 62 | @Override 63 | public DataExplorerModel getDataExplorerModel() { 64 | return null; 65 | } 66 | 67 | @Override 68 | public List getOpenImportDialogAction() { 69 | return null; 70 | } 71 | 72 | @Override 73 | public List getOpenExportDialogAction() { 74 | return null; 75 | } 76 | 77 | @Override 78 | public void importFiles(File[] files, boolean recursive) { 79 | } 80 | 81 | @Override 82 | public boolean canImportFiles() { 83 | return false; 84 | } 85 | 86 | @Override 87 | public void propertyChange(PropertyChangeEvent evt) { 88 | } 89 | 90 | @Override 91 | public String getUIName() { 92 | return NAME; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | return NAME; 98 | } 99 | 100 | @Override 101 | public String getDescription() { 102 | return DESCRIPTION; 103 | } 104 | 105 | @Override 106 | protected void changeToolWindowAnchor(CLocation clocation) { 107 | } 108 | 109 | @Override 110 | public void changingViewContentEvent(SeriesViewerEvent event) { 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/OAuth2Browser.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui; 16 | 17 | import java.util.logging.Level; 18 | import java.util.logging.Logger; 19 | 20 | import java.io.IOException; 21 | 22 | import java.net.URI; 23 | 24 | import java.awt.Toolkit; 25 | import java.awt.Desktop; 26 | import java.awt.Desktop.Action; 27 | import java.awt.datatransfer.StringSelection; 28 | 29 | import javax.swing.JOptionPane; 30 | import javax.swing.SwingUtilities; 31 | import org.weasis.dicom.google.explorer.Messages; 32 | 33 | import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; 34 | import com.google.api.client.util.Preconditions; 35 | 36 | /** 37 | * OAuth2 authorization browser that copies the authorization URL to the clipboard and shows the 38 | * notification dialog if there is no default browser configured or an error occurred while opening 39 | * the default browser. 40 | * 41 | * @author Mikhail Ukhlin 42 | * @see AuthorizationCodeInstalledApp 43 | */ 44 | public class OAuth2Browser implements AuthorizationCodeInstalledApp.Browser { 45 | 46 | private static final Logger LOGGER = 47 | Logger.getLogger(OAuth2Browser.class.getName()); 48 | 49 | /** 50 | * Single instance of this browser. 51 | */ 52 | public static final AuthorizationCodeInstalledApp.Browser INSTANCE = new OAuth2Browser(); 53 | 54 | /** 55 | * Do not allow more than one instance. 56 | */ 57 | private OAuth2Browser() { 58 | super(); 59 | } 60 | 61 | /** 62 | * Opens a browser at the given URL using {@link Desktop} if available, or alternatively copies 63 | * authorization URL to clipboard and shows notification dialog. 64 | * 65 | * @param url URL to browse. 66 | * @throws IOException if an IO error occurred. 67 | * @see AuthorizationCodeInstalledApp#browse(String) 68 | */ 69 | @Override 70 | public void browse(String url) throws IOException { 71 | Preconditions.checkNotNull(url); 72 | // Ask user to open in their browser using copy-paste 73 | System.out.println("Please open the following address in your browser:"); 74 | System.out.println(" " + url); 75 | // Attempt to open it in the browser 76 | try { 77 | if (Desktop.isDesktopSupported()) { 78 | Desktop desktop = Desktop.getDesktop(); 79 | if (desktop.isSupported(Action.BROWSE)) { 80 | System.out.println("Attempting to open that address in the default browser now..."); 81 | desktop.browse(URI.create(url)); 82 | } else { 83 | showNotification(url); 84 | } 85 | } else { 86 | showNotification(url); 87 | } 88 | } catch (IOException e) { 89 | LOGGER.log(Level.WARNING, "Unable to open browser", e); 90 | showNotification(url); 91 | } catch (InternalError e) { 92 | // A bug in a JRE can cause Desktop.isDesktopSupported() to throw an 93 | // InternalError rather than returning false. The error reads, 94 | // "Can't connect to X11 window server using ':0.0' as the value of the 95 | // DISPLAY variable." The exact error message may vary slightly. 96 | LOGGER.log(Level.WARNING, "Unable to open browser", e); 97 | showNotification(url); 98 | } 99 | } 100 | 101 | /** 102 | * Copies authorization URL to clipboard and shows notification dialog. 103 | * 104 | * @param url URL to browse. 105 | */ 106 | private static void showNotification(String url) { 107 | // Copy authorization URL to clipboard 108 | final StringSelection selection = new StringSelection(url); 109 | Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection); 110 | // Show notification dialog 111 | SwingUtilities.invokeLater(() -> { 112 | JOptionPane.showMessageDialog(null, 113 | Messages.getString("GoogleAPIClient.open_browser_message")); 114 | }); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/LoadStudiesTask.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.DicomStore; 19 | import org.weasis.dicom.google.api.model.StudyModel; 20 | import org.weasis.dicom.google.api.model.StudyQuery; 21 | import org.weasis.dicom.google.api.ui.StudyView; 22 | 23 | import java.time.LocalDate; 24 | import java.time.LocalTime; 25 | import java.time.format.DateTimeFormatter; 26 | import java.time.format.DateTimeFormatterBuilder; 27 | import java.time.temporal.ChronoField; 28 | import java.util.List; 29 | 30 | import static java.util.stream.Collectors.toList; 31 | 32 | public class LoadStudiesTask extends AbstractDicomSelectorTask> { 33 | 34 | private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); 35 | private static final DateTimeFormatter TIME_FORMAT = new DateTimeFormatterBuilder() 36 | .appendPattern("HH[:]mm[:]ss") 37 | .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) 38 | .toFormatter(); 39 | 40 | private final DicomStore store; 41 | private final StudyQuery query; 42 | 43 | 44 | public LoadStudiesTask(DicomStore store, 45 | GoogleAPIClient api, 46 | DicomStoreSelector view, 47 | StudyQuery studyQuery) { 48 | super(api, view); 49 | this.store = store; 50 | this.query = studyQuery; 51 | } 52 | 53 | @Override 54 | protected List doInBackground() throws Exception { 55 | List studies = api.fetchStudies(store, query); 56 | 57 | return studies.stream().map(this::parse).collect(toList()); 58 | } 59 | 60 | private StudyView parse(StudyModel model) { 61 | StudyView view = new StudyView(); 62 | 63 | if (model.getStudyInstanceUID() != null) { 64 | view.setStudyId(model.getStudyInstanceUID().getFirstValue().orElse("")); 65 | } 66 | if (model.getPatientName() != null) { 67 | view.setPatientName(model.getPatientName().getFirstValue().map(StudyModel.Value::getAlphabetic).orElse("")); 68 | } 69 | if (model.getPatientId() != null) { 70 | view.setPatientId(model.getPatientId().getFirstValue().orElse("")); 71 | } 72 | if (model.getAccessionNumber() != null) { 73 | view.setAccountNumber(model.getAccessionNumber().getFirstValue().orElse("")); 74 | } 75 | if (model.getStudyDate() != null) { 76 | try { 77 | view.setStudyDate(model.getStudyDate().getFirstValue().map(s -> LocalDate.parse(s, DATE_FORMAT)).orElse(null)); 78 | } catch (Exception e) { 79 | try { 80 | view.setStudyDate(model.getStudyDate().getFirstValue().map(s -> LocalDate.parse(s, DateTimeFormatter.ofPattern("yyyy.MM.dd"))).orElse(null)); 81 | } catch (Exception ignored) { 82 | } 83 | } 84 | } 85 | if (model.getStudyTime() != null) { 86 | try { 87 | view.setStudyTime(model.getStudyTime().getFirstValue().map(s -> LocalTime.parse(s, TIME_FORMAT)).orElse(null)); 88 | } catch (Exception ignored) { 89 | } 90 | } 91 | if (model.getStudyDescription() != null) { 92 | view.setDescription(model.getStudyDescription().getFirstValue().orElse("")); 93 | } 94 | if (model.getRefPhd() != null) { 95 | view.setRefPhd(model.getRefPhd().getFirstValue().map(StudyModel.Value::getAlphabetic).orElse("")); 96 | } 97 | if (model.getReqPhd() != null) { 98 | view.setReqPhd(model.getReqPhd().getFirstValue().map(StudyModel.Value::getAlphabetic).orElse("")); 99 | } 100 | if (model.getLocation() != null) { 101 | view.setLocation(model.getLocation().getFirstValue().map(StudyModel.Value::getAlphabetic).orElse("")); 102 | } 103 | if (model.getBirthDate() != null) { 104 | try { 105 | view.setBirthDate(model.getBirthDate().getFirstValue().map(s -> LocalDate.parse(s, DATE_FORMAT)).orElse(null)); 106 | } catch (Exception ignored) { 107 | } 108 | } 109 | 110 | return view; 111 | } 112 | 113 | @Override 114 | protected void onCompleted(List result) { 115 | view.updateTable(result); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/StudiesTable.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import javax.swing.JPanel; 21 | import javax.swing.JTable; 22 | import javax.swing.JScrollPane; 23 | import javax.swing.ImageIcon; 24 | import javax.swing.table.DefaultTableModel; 25 | import javax.swing.table.TableModel; 26 | 27 | import java.awt.BorderLayout; 28 | import java.awt.Font; 29 | import java.awt.Image; 30 | import java.awt.Point; 31 | import java.awt.event.MouseAdapter; 32 | import java.awt.event.MouseEvent; 33 | import java.awt.image.ImageObserver; 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | import java.util.Objects; 37 | import java.util.Vector; 38 | 39 | public class StudiesTable extends JPanel { 40 | 41 | private static final Logger LOGGER = LoggerFactory.getLogger(StudiesTable.class); 42 | 43 | private ImageIcon loadIcon = new ImageIcon(this.getClass().getResource("/loading.gif")); 44 | 45 | private static final Object[] COLUMN_NAMES = { 46 | "Status","Patient name", "Patient ID", "ACC.#", "Study date", "Study time", 47 | "Desc", "REF.PHD", "REQ.PHD", "LOCATION", "BIRTH DATE" 48 | }; 49 | 50 | private final DefaultTableModel tableModel; 51 | 52 | private final List studies = new ArrayList<>(); 53 | private final GoogleExplorer explorer; 54 | private final JTable table; 55 | 56 | public StudiesTable(GoogleExplorer explorer) { 57 | this.explorer = explorer; 58 | tableModel = new DefaultTableModel(COLUMN_NAMES, 0) { 59 | @Override 60 | public boolean isCellEditable(int row, int column) { 61 | return false; 62 | } 63 | @Override 64 | public Class getColumnClass(int columnIndex) { 65 | if (columnIndex == 0) { 66 | return ImageIcon.class; 67 | } 68 | return super.getColumnClass(columnIndex); 69 | } 70 | }; 71 | 72 | table = new JTable(tableModel); 73 | table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); 74 | JScrollPane scrollPane = new JScrollPane(table); 75 | table.setFillsViewportHeight(true); 76 | table.setFont(new Font("Sans-serif", Font.PLAIN, 14)); 77 | 78 | BorderLayout layout = new BorderLayout(); 79 | setLayout(layout); 80 | add(scrollPane, BorderLayout.CENTER); 81 | 82 | table.addMouseListener(new MouseAdapter() { 83 | public void mousePressed(MouseEvent mouseEvent) { 84 | JTable table = (JTable) mouseEvent.getSource(); 85 | Point point = mouseEvent.getPoint(); 86 | int row = table.rowAtPoint(point); 87 | if (mouseEvent.getClickCount() == 2 88 | && row >= 0) { 89 | StudyView study = studies.get(row); 90 | explorer.fireStudySelected(study.getStudyId()); 91 | } 92 | } 93 | }); 94 | } 95 | 96 | private void setImageObserver(JTable table) { 97 | TableModel model = table.getModel(); 98 | int rowCount = model.getRowCount(); 99 | int col = 0; 100 | if (ImageIcon.class == model.getColumnClass(col)) { 101 | for (int row = 0; row < rowCount; row++) { 102 | Object obj = model.getValueAt(row, col); 103 | ImageIcon icon = null; 104 | if (obj instanceof ImageIcon) { 105 | icon = (ImageIcon) model.getValueAt(row, col); 106 | } 107 | if (icon != null && icon.getImageObserver() == null) { 108 | icon.setImageObserver(new CellImageObserver(table, row, col)); 109 | } 110 | } 111 | } 112 | } 113 | 114 | class CellImageObserver implements ImageObserver { 115 | JTable table; 116 | int row; 117 | int col; 118 | 119 | CellImageObserver(JTable table, int row, int col) { 120 | this.table = table; 121 | this.row = row; 122 | this.col = col; 123 | } 124 | 125 | public boolean imageUpdate(Image img, int flags, int x, int y, int w, int h) { 126 | if ((flags & (FRAMEBITS | ALLBITS)) != 0) { 127 | table.repaint(); 128 | } 129 | return (flags & (ALLBITS | ABORT)) == 0; 130 | } 131 | } 132 | 133 | public void showLoadIcon(String studyId) { 134 | for (int i=0;i < studies.size();i++) { 135 | if (Objects.equals(studies.get(i).getStudyId(), studyId)) { 136 | table.setValueAt(loadIcon, i, 0); 137 | break; 138 | } 139 | } 140 | setImageObserver(table); 141 | } 142 | 143 | public void hideLoadIcon(String studyId) { 144 | for (int i=0;i < studies.size();i++) { 145 | if (Objects.equals(studies.get(i).getStudyId(), studyId)) { 146 | table.setValueAt(null, i, 0); 147 | break; 148 | } 149 | } 150 | setImageObserver(table); 151 | } 152 | 153 | public void addStudy(StudyView study) { 154 | LOGGER.info("Adding record"); 155 | Vector values = new Vector<>(); 156 | values.add(null); 157 | values.add(study.getPatientName()); 158 | values.add(study.getPatientId()); 159 | values.add(study.getAccountNumber()); 160 | values.add(study.getStudyDate()); 161 | values.add(study.getStudyTime()); 162 | values.add(study.getDescription()); 163 | values.add(study.getRefPhd()); 164 | values.add(study.getReqPhd()); 165 | values.add(study.getLocation()); 166 | values.add(study.getBirthDate()); 167 | tableModel.addRow(values); 168 | studies.add(study); 169 | } 170 | 171 | public void clearTable() { 172 | LOGGER.info("Removing " + tableModel.getRowCount() + " records"); 173 | for (int i = tableModel.getRowCount() - 1; i >= 0; i--) { 174 | tableModel.removeRow(i); 175 | } 176 | studies.clear(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/model/StudyModel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.model; 16 | 17 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | import java.util.List; 21 | import java.util.Optional; 22 | 23 | @JsonIgnoreProperties(ignoreUnknown = true) 24 | public class StudyModel { 25 | 26 | @JsonProperty("0020000D") 27 | private RecordPlain studyInstanceUID; 28 | 29 | @JsonProperty("00100010") 30 | private RecordObjects patientName; 31 | 32 | @JsonProperty("00100020") 33 | private RecordPlain patientId; 34 | 35 | @JsonProperty("00080050") 36 | private RecordPlain accessionNumber; 37 | 38 | @JsonProperty("00080020") 39 | private RecordPlain studyDate; 40 | 41 | @JsonProperty("00080030") 42 | private RecordPlain studyTime; 43 | 44 | @JsonProperty("00081030") 45 | private RecordPlain studyDescription; 46 | 47 | @JsonProperty("00321032") 48 | private RecordObjects reqPhd; 49 | 50 | @JsonProperty("00080090") 51 | private RecordObjects refPhd; 52 | 53 | @JsonProperty("00200050") 54 | private RecordObjects location; 55 | 56 | @JsonProperty("00100030") 57 | private RecordPlain birthDate; 58 | 59 | public RecordPlain getStudyInstanceUID() { 60 | return studyInstanceUID; 61 | } 62 | 63 | public void setStudyInstanceUID(RecordPlain studyInstanceUID) { 64 | this.studyInstanceUID = studyInstanceUID; 65 | } 66 | 67 | public RecordObjects getPatientName() { 68 | return patientName; 69 | } 70 | 71 | public void setPatientName(RecordObjects patientName) { 72 | this.patientName = patientName; 73 | } 74 | 75 | public RecordPlain getPatientId() { 76 | return patientId; 77 | } 78 | 79 | public void setPatientId(RecordPlain patientId) { 80 | this.patientId = patientId; 81 | } 82 | 83 | public RecordPlain getAccessionNumber() { 84 | return accessionNumber; 85 | } 86 | 87 | public void setAccessionNumber(RecordPlain accessionNumber) { 88 | this.accessionNumber = accessionNumber; 89 | } 90 | 91 | public RecordPlain getStudyDate() { 92 | return studyDate; 93 | } 94 | 95 | public void setStudyDate(RecordPlain studyDate) { 96 | this.studyDate = studyDate; 97 | } 98 | 99 | public RecordPlain getStudyTime() { 100 | return studyTime; 101 | } 102 | 103 | public void setStudyTime(RecordPlain studyTime) { 104 | this.studyTime = studyTime; 105 | } 106 | 107 | public RecordPlain getStudyDescription() { 108 | return studyDescription; 109 | } 110 | 111 | public void setStudyDescription(RecordPlain studyDescription) { 112 | this.studyDescription = studyDescription; 113 | } 114 | 115 | public RecordObjects getReqPhd() { 116 | return reqPhd; 117 | } 118 | 119 | public void setReqPhd(RecordObjects reqPhd) { 120 | this.reqPhd = reqPhd; 121 | } 122 | 123 | public RecordObjects getRefPhd() { 124 | return refPhd; 125 | } 126 | 127 | public void setRefPhd(RecordObjects refPhd) { 128 | this.refPhd = refPhd; 129 | } 130 | 131 | public RecordObjects getLocation() { 132 | return location; 133 | } 134 | 135 | public void setLocation(RecordObjects location) { 136 | this.location = location; 137 | } 138 | 139 | public RecordPlain getBirthDate() { 140 | return birthDate; 141 | } 142 | 143 | public void setBirthDate(RecordPlain birthDate) { 144 | this.birthDate = birthDate; 145 | } 146 | 147 | @JsonIgnoreProperties(ignoreUnknown = true) 148 | public static class RecordPlain { 149 | private String vr; 150 | 151 | @JsonProperty("Value") 152 | private List value; 153 | 154 | public String getVr() { 155 | return vr; 156 | } 157 | 158 | public void setVr(String vr) { 159 | this.vr = vr; 160 | } 161 | 162 | public List getValue() { 163 | return value; 164 | } 165 | 166 | public void setValue(List value) { 167 | this.value = value; 168 | } 169 | 170 | public Optional getFirstValue() { 171 | if (value == null || value.size() == 0) { 172 | return Optional.empty(); 173 | } else { 174 | return Optional.ofNullable(value.get(0)); 175 | } 176 | } 177 | } 178 | 179 | @JsonIgnoreProperties(ignoreUnknown = true) 180 | public static class RecordObjects { 181 | private String vr; 182 | 183 | @JsonProperty("Value") 184 | private List value; 185 | 186 | public String getVr() { 187 | return vr; 188 | } 189 | 190 | public void setVr(String vr) { 191 | this.vr = vr; 192 | } 193 | 194 | public List getValue() { 195 | return value; 196 | } 197 | 198 | public void setValue(List value) { 199 | this.value = value; 200 | } 201 | 202 | public Optional getFirstValue() { 203 | if (value == null || value.size() == 0) { 204 | return Optional.empty(); 205 | } else { 206 | return Optional.ofNullable(value.get(0)); 207 | } 208 | } 209 | } 210 | 211 | @JsonIgnoreProperties(ignoreUnknown = true) 212 | public static class Value { 213 | 214 | @JsonProperty("Alphabetic") 215 | private String alphabetic; 216 | 217 | @JsonProperty("Ideographic") 218 | private String ideographic; 219 | 220 | @JsonProperty("Phonetic") 221 | private String phonetic; 222 | 223 | public String getAlphabetic() { 224 | return alphabetic; 225 | } 226 | 227 | public void setAlphabetic(String alphabetic) { 228 | this.alphabetic = alphabetic; 229 | } 230 | 231 | public String getIdeographic() { 232 | return ideographic; 233 | } 234 | 235 | public void setIdeographic(String ideographic) { 236 | this.ideographic = ideographic; 237 | } 238 | 239 | public String getPhonetic() { 240 | return phonetic; 241 | } 242 | 243 | public void setPhonetic(String phonetic) { 244 | this.phonetic = phonetic; 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 3.0.13 5 | 6 | weasis-dicom-parent 7 | org.weasis.dicom 8 | 3.6.0 9 | 10 | 4.0.0 11 | weasis-chcapi-extension 12 | bundle 13 | Google Healthcare DICOM Explorer [${project.artifactId}] 14 | 15 | org.weasis.dicom.google.explorer 16 | 1.28.0 17 | 2.8.5 18 | 2.10.0.pr1 19 | 1.3.4 20 | 2.0.2 21 | 3.6.0 22 | 23 | 24 | 25 | 26 | 27 | org.apache.felix 28 | maven-scr-plugin 29 | 30 | 31 | org.apache.felix 32 | maven-bundle-plugin 33 | 34 | 35 | ${bundle.namespace}.internal.Activator 36 | javax.portlet;resolution:=optional, 37 | org.apache.avalon.*;resolution:=optional, 38 | org.apache.log.*;resolution:=optional, 39 | org.apache.log4j.*;resolution:=optional, 40 | com.google.appengine.api;resolution:=optional, 41 | com.google.apphosting.api;resolution:=optional, 42 | io.grpc.override;resolution:=optional, 43 | javax.naming.*;resolution:=optional, 44 | org.ietf.jgss;resolution:=optional, 45 | * 46 | 47 | *;scope=compile;inline=true 48 | true 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.weasis.core 57 | weasis-core-api 58 | ${weasis.version} 59 | provided 60 | 61 | 62 | org.weasis.core 63 | weasis-core-ui 64 | ${weasis.version} 65 | provided 66 | 67 | 68 | org.weasis.thirdparty 69 | docking-frames 70 | provided 71 | 72 | 73 | org.weasis.dicom 74 | weasis-dicom-codec 75 | ${weasis.version} 76 | provided 77 | 78 | 79 | 80 | com.google.code.gson 81 | gson 82 | ${gson.version} 83 | 84 | 85 | com.fasterxml.jackson.core 86 | jackson-databind 87 | ${jackson.version} 88 | 89 | 90 | commons-fileupload 91 | commons-fileupload 92 | 1.3.3 93 | 94 | 95 | org.jdatepicker 96 | jdatepicker 97 | ${jdatepicker.version} 98 | 99 | 100 | com.google.api-client 101 | google-api-client 102 | ${google.api.version} 103 | 104 | 105 | com.google.oauth-client 106 | google-oauth-client-jetty 107 | ${google.api.version} 108 | 109 | 110 | com.google.oauth-client 111 | google-oauth-client 112 | 113 | 114 | 115 | 116 | com.google.apis 117 | google-api-services-oauth2 118 | v2-rev141-1.25.0 119 | 120 | 121 | com.google.api-client 122 | google-api-client 123 | 124 | 125 | 126 | 127 | com.google.apis 128 | google-api-services-cloudresourcemanager 129 | v1-rev502-1.25.0 130 | 131 | 132 | com.google.api-client 133 | google-api-client 134 | 135 | 136 | 137 | 138 | junit 139 | junit 140 | 4.13.1 141 | test 142 | 143 | 144 | org.powermock 145 | powermock-module-junit4 146 | ${powermock.version} 147 | test 148 | 149 | 150 | org.powermock 151 | powermock-api-mockito2 152 | ${powermock.version} 153 | test 154 | 155 | 156 | org.apache.logging.log4j 157 | log4j-core 158 | 2.17.1 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/explorer/DownloadManager.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.explorer; 16 | 17 | import org.apache.commons.fileupload.MultipartStream; 18 | import org.dcm4che3.data.Tag; 19 | import org.slf4j.LoggerFactory; 20 | import org.weasis.core.api.explorer.model.DataExplorerModel; 21 | import org.weasis.core.api.gui.util.AppProperties; 22 | import org.weasis.core.api.media.MimeInspector; 23 | import org.weasis.core.api.media.data.*; 24 | import org.weasis.core.api.util.FileUtil; 25 | import org.weasis.core.api.util.ThreadUtil; 26 | import org.weasis.core.ui.editor.FileModel; 27 | import org.weasis.core.ui.editor.ViewerPluginBuilder; 28 | import org.weasis.dicom.codec.DicomMediaIO; 29 | import org.weasis.dicom.codec.DicomCodec; 30 | import org.weasis.dicom.codec.TagD; 31 | import org.weasis.dicom.google.api.GoogleAPIClient; 32 | import com.google.api.client.http.HttpHeaders; 33 | import com.google.api.client.http.HttpResponse; 34 | import com.google.api.client.http.HttpStatusCodes; 35 | import javax.swing.SwingWorker; 36 | import java.io.File; 37 | import java.io.FileOutputStream; 38 | import java.io.OutputStream; 39 | import java.util.*; 40 | import java.util.concurrent.ExecutorService; 41 | import java.util.stream.Collectors; 42 | 43 | public class DownloadManager { 44 | 45 | static final ExecutorService LOADING_EXECUTOR = ThreadUtil.buildNewFixedThreadExecutor(2, "Google Dicom Explorer"); //$NON-NLS-1$ 46 | 47 | public static ExecutorService getLoadingExecutor() { 48 | return LOADING_EXECUTOR; 49 | } 50 | 51 | public interface DownloadListener { 52 | public void downloadFinished(); 53 | } 54 | 55 | public static class LoadGoogleDicom extends SwingWorker { 56 | 57 | private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(LoadGoogleDicom.class); 58 | private final GoogleAPIClient client; 59 | private File[] files; 60 | private final String url; 61 | private final FileModel dicomModel; 62 | private static final Map fileCache = new HashMap<>(); 63 | public static final File DICOM_TMP_DIR = AppProperties.buildAccessibleTempDirectory("gcp_cache"); //$NON-NLS-1$ 64 | private DownloadListener downloadListener; 65 | 66 | 67 | public LoadGoogleDicom(String url, DataExplorerModel explorerModel, GoogleAPIClient client, DownloadListener listener) { 68 | this.url = url; 69 | this.dicomModel = ViewerPluginBuilder.DefaultDataModel; 70 | this.client = client; 71 | this.downloadListener = listener; 72 | } 73 | 74 | @Override 75 | protected Boolean doInBackground() throws Exception { 76 | if (url == null) { 77 | throw new IllegalArgumentException("invalid parameters"); //$NON-NLS-1$ 78 | } 79 | if (fileCache.containsKey(url)) { 80 | LOGGER.info("Loading from local cache"); 81 | files = fileCache.get(url); 82 | } else { 83 | LOGGER.info("Loading from Google Healthcare API"); 84 | files = downloadFiles(url); 85 | fileCache.put(url, files); 86 | } 87 | LOGGER.debug(Arrays.stream(files).map(f -> f.getName()).collect(Collectors.joining("\n"))); 88 | 89 | addSelectionAndnotify(files); 90 | return true; 91 | } 92 | 93 | @Override 94 | protected void done() { 95 | LOGGER.info("End of loading DICOM from Google Healthcare API"); //$NON-NLS-1$ 96 | downloadListener.downloadFinished(); 97 | } 98 | 99 | public void addSelectionAndnotify(File[] file) { 100 | if (file == null || file.length < 1) { 101 | return; 102 | } 103 | 104 | for (File file1 : file) { 105 | if (isCancelled()) { 106 | LOGGER.info("Download cancelled, returning"); 107 | return; 108 | } 109 | 110 | if (file1 == null) { 111 | continue; 112 | 113 | } else { 114 | 115 | if (file1.canRead()) { 116 | if (FileUtil.isFileExtensionMatching(file1, DicomCodec.FILE_EXTENSIONS) 117 | || MimeInspector.isMatchingMimeTypeFromMagicNumber(file1, DicomMediaIO.DICOM_MIMETYPE)) { 118 | DicomMediaIO loader = new DicomMediaIO(file1); 119 | if (loader.isReadableDicom()) { 120 | ViewerPluginBuilder.openSequenceInDefaultPlugin(loader.getMediaSeries(), dicomModel, false, false); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | private File[] downloadFiles(String dicomUrl) { 129 | try { 130 | final HttpHeaders headers = new HttpHeaders(); 131 | headers.setAccept("multipart/related; type=application/dicom; transfer-syntax=*"); 132 | final HttpResponse response = client.executeGetRequest(dicomUrl, headers); 133 | final int responseCode = response.getStatusCode(); 134 | 135 | if (responseCode == HttpStatusCodes.STATUS_CODE_OK) { 136 | String contentType = response.getContentType(); 137 | //find multipart boundary of multipart/related response 138 | int indexStart = contentType.indexOf("boundary=") + 9; 139 | int indexEnd = contentType.indexOf(";", indexStart + 1); 140 | if (indexEnd == -1) { 141 | indexEnd = contentType.length() - 1; 142 | } 143 | String boundary = contentType.substring(indexStart, indexEnd); 144 | 145 | MultipartStream multipart = new MultipartStream(response.getContent(), boundary.getBytes()); 146 | boolean nextPart = multipart.skipPreamble(); 147 | 148 | ArrayList files = new ArrayList<>(); 149 | long start = System.currentTimeMillis(); 150 | while (nextPart) { 151 | File outFile = File.createTempFile("gcp_", ".dcm", getDicomTmpDir()); //$NON-NLS-1$ //$NON-NLS-2$ 152 | String header = multipart.readHeaders(); 153 | 154 | try (OutputStream output = new FileOutputStream(outFile)) { 155 | multipart.readBodyData(output); 156 | } 157 | files.add(outFile); 158 | nextPart = multipart.readBoundary(); 159 | } 160 | LOGGER.debug("Elapsed time: {} ", System.currentTimeMillis() - start); 161 | return files.toArray(new File[0]); 162 | } else { 163 | throw new RuntimeException("Error processing HTTP request. Response code: " + responseCode); 164 | } 165 | } catch (Exception e) { 166 | LOGGER.error("Error occured ", e); 167 | throw new RuntimeException(e); 168 | } 169 | } 170 | 171 | // Solves missing tmp folder problem (on Windows). 172 | private static File getDicomTmpDir() { 173 | if (!DICOM_TMP_DIR.exists()) { 174 | LOGGER.info("DICOM tmp dir not found. Re-creating it."); //$NON-NLS-1$ 175 | AppProperties.buildAccessibleTempDirectory("gcp_cache"); //$NON-NLS-1$ 176 | } 177 | return DICOM_TMP_DIR; 178 | } 179 | } 180 | 181 | private static final Comparator instanceNumberComparator = (m1, m2) -> { 182 | Integer val1 = TagD.getTagValue(m1, Tag.InstanceNumber, Integer.class); 183 | Integer val2 = TagD.getTagValue(m2, Tag.InstanceNumber, Integer.class); 184 | if (val1 == null || val2 == null) { 185 | return 0; 186 | } 187 | return val1.compareTo(val2); 188 | }; 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/SearchPanel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui; 16 | 17 | import org.weasis.dicom.google.api.model.StudyQuery; 18 | import org.weasis.dicom.google.api.ui.dicomstore.DicomStoreSelector; 19 | import org.jdatepicker.DateModel; 20 | import org.jdatepicker.impl.JDatePanelImpl; 21 | import org.jdatepicker.impl.JDatePickerImpl; 22 | import org.jdatepicker.impl.UtilDateModel; 23 | 24 | import javax.swing.JCheckBox; 25 | import javax.swing.border.Border; 26 | import javax.swing.BoxLayout; 27 | import javax.swing.Box; 28 | import javax.swing.BorderFactory; 29 | import javax.swing.JFormattedTextField; 30 | import javax.swing.JButton; 31 | import javax.swing.JLabel; 32 | import javax.swing.JPanel; 33 | import javax.swing.JTextField; 34 | import javax.swing.SwingConstants; 35 | 36 | import java.awt.Color; 37 | import java.awt.Dimension; 38 | import java.awt.Font; 39 | import java.text.ParseException; 40 | import java.text.SimpleDateFormat; 41 | import java.time.LocalDate; 42 | import java.util.Calendar; 43 | import java.util.Properties; 44 | 45 | import static javax.swing.BoxLayout.LINE_AXIS; 46 | import static javax.swing.BoxLayout.PAGE_AXIS; 47 | import static javax.swing.BoxLayout.X_AXIS; 48 | 49 | public class SearchPanel extends JPanel { 50 | private static final int PAGE_SIZE = 100; 51 | private static final int DEFAULT_PAGE = 0; 52 | private static final String DEFAULT_PAGE_PREFIX = "Page "; 53 | private static final String DEFAULT_PAGE_LABEL = DEFAULT_PAGE_PREFIX + DEFAULT_PAGE; 54 | private final JLabel pageNumberLabel = label("Page 0"); 55 | private final JButton pageNumberButtonNext = new JButton("Next"); 56 | private final JButton pageNumberButtonPrevious = new JButton("Prev"); 57 | private final JTextField patientName = textField(); 58 | private final JCheckBox fuzzyMatching = textBox("fuzzy match", false); 59 | private final JTextField patientId = textField(); 60 | private final JDatePickerImpl startDate = createDatePicker(); 61 | private final JDatePickerImpl endDate = createDatePicker(); 62 | private final JTextField accessionNumber = textField(); 63 | private final JTextField referringPhd = textField(); 64 | 65 | private final DicomStoreSelector storeSelector; 66 | private int pageNumber; 67 | 68 | public SearchPanel(DicomStoreSelector storeSelector) { 69 | this.storeSelector = storeSelector; 70 | initSearchPanel(); 71 | } 72 | 73 | private void initSearchPanel() { 74 | BoxLayout layout = new BoxLayout(this, PAGE_AXIS); 75 | setLayout(layout); 76 | Border border = BorderFactory.createCompoundBorder( 77 | BorderFactory.createMatteBorder(0, 0, 0, 2, Color.GRAY), 78 | BorderFactory.createEmptyBorder(5, 5, 5, 15)); 79 | setBorder(border); 80 | setPreferredSize(new Dimension(300, 300)); 81 | 82 | JLabel searchLabel = label("Search"); 83 | searchLabel.setFont(new Font("Sans-serif", Font.BOLD, 18)); 84 | add(searchLabel); 85 | 86 | add(Box.createVerticalStrut(10)); 87 | 88 | JLabel studyDateLabel = label("Study date"); 89 | add(studyDateLabel); 90 | JPanel datePanel = new JPanel(); 91 | BoxLayout datePanelLayout = new BoxLayout(datePanel, LINE_AXIS); 92 | datePanel.setLayout(datePanelLayout); 93 | datePanel.add(startDate); 94 | datePanel.add(endDate); 95 | add(datePanel); 96 | 97 | add(Box.createVerticalStrut(20)); 98 | 99 | JLabel patientNameLabel = label("Patient Name"); 100 | add(patientNameLabel); 101 | add(patientName); 102 | JPanel fuzzyMatchingPanel = new JPanel(); 103 | BoxLayout fuzzyMatchingPanelLayout = new BoxLayout(fuzzyMatchingPanel, X_AXIS); 104 | fuzzyMatchingPanel.setLayout(fuzzyMatchingPanelLayout); 105 | fuzzyMatchingPanel.add(fuzzyMatching); 106 | add(fuzzyMatchingPanel); 107 | 108 | add(Box.createVerticalStrut(20)); 109 | 110 | JLabel patientIdLabel = label("Patient ID"); 111 | add(patientIdLabel); 112 | add(patientId); 113 | 114 | add(Box.createVerticalStrut(20)); 115 | 116 | JLabel accessionNumberLabel = label("Accession number"); 117 | add(accessionNumberLabel); 118 | add(accessionNumber); 119 | 120 | add(Box.createVerticalStrut(20)); 121 | 122 | JLabel referringPhdLabel = label("Referring Physician"); 123 | add(referringPhdLabel); 124 | add(referringPhd); 125 | 126 | add(Box.createVerticalStrut(20)); 127 | 128 | JButton searchButton = new JButton("Search"); 129 | searchButton.addActionListener((action) -> reloadTable()); 130 | JButton reset = new JButton("Reset"); 131 | setPageNumber(DEFAULT_PAGE); 132 | 133 | reset.addActionListener((action) -> { 134 | clearSearchForm(); 135 | reloadTable(); 136 | pageNumberLabel.setText(DEFAULT_PAGE_LABEL); 137 | }); 138 | 139 | storeSelector.addStoreListener((event) -> { 140 | reloadTable(); 141 | }); 142 | 143 | pageNumberButtonPrevious.addActionListener((action) -> { 144 | previousPage(); 145 | }); 146 | 147 | pageNumberButtonNext.addActionListener((action) -> { 148 | nextPage(); 149 | }); 150 | pageNumberLabel.setVisible(false); 151 | pageNumberButtonNext.setVisible(false); 152 | pageNumberButtonPrevious.setVisible(false); 153 | 154 | JPanel buttonPanel = new JPanel(); 155 | BoxLayout buttonPanelLayout = new BoxLayout(buttonPanel, LINE_AXIS); 156 | buttonPanel.setLayout(buttonPanelLayout); 157 | buttonPanel.add(searchButton); 158 | buttonPanel.add(Box.createHorizontalGlue()); 159 | buttonPanel.add(reset); 160 | add(buttonPanel); 161 | 162 | add(Box.createVerticalGlue()); 163 | } 164 | 165 | public void loadTable(int page) { 166 | storeSelector.loadStudies(buildQuery(page)); 167 | } 168 | 169 | public void reloadTable() { 170 | setPageNumber(0); 171 | loadTable(pageNumber); 172 | pageNumberLabel.setVisible(true); 173 | pageNumberButtonNext.setVisible(true); 174 | } 175 | 176 | 177 | public void previousPage() { 178 | if (this.pageNumber > 0) { 179 | setPageNumber(pageNumber - 1); 180 | loadTable(pageNumber); 181 | } 182 | } 183 | 184 | public void nextPage() { 185 | setPageNumber(pageNumber + 1); 186 | loadTable(pageNumber); 187 | } 188 | 189 | private void clearSearchForm() { 190 | patientName.setText(""); 191 | patientId.setText(""); 192 | accessionNumber.setText(""); 193 | referringPhd.setText(""); 194 | startDate.getModel().setSelected(false); 195 | endDate.getModel().setSelected(false); 196 | } 197 | 198 | private StudyQuery buildQuery(int page) { 199 | StudyQuery query = new StudyQuery(); 200 | query.setPatientName(patientName.getText()); 201 | query.setFuzzyMatching(fuzzyMatching.isSelected()); 202 | query.setPatientId(patientId.getText()); 203 | query.setAccessionNumber(accessionNumber.getText()); 204 | query.setPhysicianName(referringPhd.getText()); 205 | query.setPage(page); 206 | query.setPageSize(PAGE_SIZE); 207 | DateModel startDateModel = startDate.getModel(); 208 | if (startDateModel.isSelected()) { 209 | query.setStartDate(LocalDate.of(startDateModel.getYear(), startDateModel.getMonth() + 1, startDateModel.getDay())); 210 | } 211 | 212 | DateModel endDateModel = endDate.getModel(); 213 | if (endDateModel.isSelected()) { 214 | query.setEndDate(LocalDate.of(endDateModel.getYear(), endDateModel.getMonth() + 1, endDateModel.getDay())); 215 | } 216 | 217 | return query; 218 | } 219 | 220 | private JDatePickerImpl createDatePicker() { 221 | UtilDateModel model = new UtilDateModel(); 222 | Properties p = new Properties(); 223 | p.put("text.today", "Today"); 224 | p.put("text.month", "Month"); 225 | p.put("text.year", "Year"); 226 | JDatePanelImpl datePanel = new JDatePanelImpl(model, p); 227 | return new JDatePickerImpl(datePanel, new DateLabelFormatter()); 228 | } 229 | 230 | private JTextField textField() { 231 | JTextField result = new JTextField(); 232 | result.setMaximumSize( 233 | new Dimension(Integer.MAX_VALUE, result.getPreferredSize().height)); 234 | 235 | return result; 236 | } 237 | 238 | private JCheckBox textBox(String text, boolean selected) { 239 | JCheckBox checkBox = new JCheckBox(text, selected); 240 | checkBox.setMaximumSize( 241 | new Dimension(Integer.MAX_VALUE, checkBox.getPreferredSize().height)); 242 | return checkBox; 243 | } 244 | 245 | private JLabel label(String text) { 246 | JLabel result = new JLabel(text); 247 | 248 | result.setAlignmentX(CENTER_ALIGNMENT); 249 | result.setHorizontalTextPosition(SwingConstants.CENTER); 250 | 251 | return result; 252 | } 253 | 254 | private void setPageNumber(int pageNumber) { 255 | this.pageNumber = pageNumber; 256 | this.pageNumberLabel.setText(DEFAULT_PAGE_PREFIX + String.valueOf(pageNumber)); 257 | if (pageNumber == DEFAULT_PAGE) { 258 | pageNumberButtonPrevious.setVisible(false); 259 | } else { 260 | pageNumberButtonPrevious.setVisible(true); 261 | } 262 | } 263 | 264 | public JLabel getPageNumberLabel() { 265 | return pageNumberLabel; 266 | } 267 | 268 | public JButton getPageNumberButtonNext() { 269 | return pageNumberButtonNext; 270 | } 271 | 272 | public JButton getPageNumberButtonPrevious() { 273 | return pageNumberButtonPrevious; 274 | } 275 | 276 | 277 | public class DateLabelFormatter extends JFormattedTextField.AbstractFormatter { 278 | 279 | private String datePattern = "yyyy-MM-dd"; 280 | private SimpleDateFormat dateFormatter = new SimpleDateFormat(datePattern); 281 | 282 | @Override 283 | public Object stringToValue(String text) throws ParseException { 284 | return dateFormatter.parseObject(text); 285 | } 286 | 287 | @Override 288 | public String valueToString(Object value) throws ParseException { 289 | if (value != null) { 290 | Calendar cal = (Calendar) value; 291 | return dateFormatter.format(cal.getTime()); 292 | } 293 | 294 | return ""; 295 | } 296 | 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/test/java/org/weasis/dicom/google/api/GoogleAPIClientTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api; 16 | 17 | import com.google.api.client.http.HttpRequest; 18 | import com.google.api.client.http.HttpTransport; 19 | import com.google.api.client.http.LowLevelHttpRequest; 20 | import com.google.api.client.http.LowLevelHttpResponse; 21 | import com.google.api.client.json.Json; 22 | import com.google.api.client.testing.http.HttpTesting; 23 | import com.google.api.client.testing.http.MockHttpTransport; 24 | import com.google.api.client.testing.http.MockLowLevelHttpRequest; 25 | import com.google.api.client.testing.http.MockLowLevelHttpResponse; 26 | import com.google.api.services.cloudresourcemanager.CloudResourceManager; 27 | import com.google.api.services.cloudresourcemanager.CloudResourceManager.Projects; 28 | import com.google.api.services.cloudresourcemanager.model.ListProjectsResponse; 29 | import com.google.api.services.cloudresourcemanager.model.Project; 30 | import java.io.IOException; 31 | import java.lang.reflect.Field; 32 | import java.util.ArrayList; 33 | import java.util.Arrays; 34 | import java.util.List; 35 | import org.junit.Test; 36 | import org.junit.BeforeClass; 37 | import org.junit.runner.RunWith; 38 | 39 | import org.mockito.Mockito; 40 | import org.mockito.stubbing.Answer; 41 | import org.mockito.invocation.InvocationOnMock; 42 | 43 | import org.powermock.api.mockito.PowerMockito; 44 | import org.powermock.core.classloader.annotations.PowerMockIgnore; 45 | import org.powermock.modules.junit4.PowerMockRunner; 46 | import org.weasis.dicom.google.api.model.Dataset; 47 | import org.weasis.dicom.google.api.model.DicomStore; 48 | import org.weasis.dicom.google.api.model.Location; 49 | import org.weasis.dicom.google.api.model.ProjectDescriptor; 50 | import org.weasis.dicom.google.api.model.StudyQuery; 51 | import org.powermock.core.classloader.annotations.PrepareForTest; 52 | 53 | import com.google.api.client.http.HttpHeaders; 54 | import com.google.api.client.http.HttpResponse; 55 | import com.google.api.client.http.HttpStatusCodes; 56 | import com.google.api.client.http.HttpResponseException; 57 | import com.google.api.client.http.HttpResponseException.Builder; 58 | 59 | import static org.junit.Assert.assertEquals; 60 | import static org.powermock.api.mockito.PowerMockito.mock; 61 | import static org.powermock.api.mockito.PowerMockito.when; 62 | 63 | /** 64 | * Tests for {@link GoogleAPIClient} class. 65 | * 66 | * @author Mikhail Ukhlin 67 | */ 68 | @PowerMockIgnore("javax.swing.*") 69 | @RunWith(PowerMockRunner.class) 70 | @PrepareForTest(GoogleAPIClient.class) 71 | public class GoogleAPIClientTest { 72 | 73 | /** 74 | * Sets up test environment. 75 | */ 76 | @BeforeClass 77 | public static void setup() { 78 | // This is necessary to initialize static fields 79 | System.setProperty("weasis.resources.path", "/"); 80 | } 81 | 82 | /** 83 | * Tests {@link GoogleAPIClient#executeGetRequest(String)} method for token expired case. 84 | */ 85 | @Test 86 | public void testTokenExpired() throws Exception { 87 | // Given 88 | final String url = "https://test.com/test"; 89 | final GoogleAPIClient client = PowerMockito.spy( 90 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 91 | 92 | // When 93 | Mockito.doReturn("TEST-TOKEN-1").when(client).signIn(); 94 | Mockito.doReturn("TEST-TOKEN-2").when(client).refresh(); 95 | 96 | PowerMockito.doAnswer(new Answer() { 97 | boolean tokenExpired = true; 98 | @Override public HttpResponse answer(InvocationOnMock invocation) throws Throwable { 99 | // Throw exception first time emulating token expired and return null second time 100 | if (tokenExpired) { 101 | tokenExpired = false; 102 | throw new HttpResponseException( 103 | new Builder(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED, "TEST", new HttpHeaders())) { 104 | private static final long serialVersionUID = -1765357098467474492L; 105 | }; 106 | } 107 | // Cannot return HTTPResponse because it has package private constructor 108 | return null; 109 | } 110 | }).when(client, "doExecuteGetRequest", Mockito.anyString(), Mockito.any()); 111 | 112 | client.executeGetRequest(url); 113 | 114 | // Then 115 | Mockito.verify(client, Mockito.times(1)).signIn(); 116 | Mockito.verify(client, Mockito.times(1)).refresh(); 117 | Mockito.verify(client, Mockito.times(1)).executeGetRequest(Mockito.eq(url)); 118 | Mockito.verify(client, Mockito.times(1)).executeGetRequest(Mockito.eq(url), Mockito.any()); 119 | PowerMockito.verifyPrivate(client, Mockito.times(2)).invoke("doExecuteGetRequest", 120 | Mockito.eq(url), Mockito.any()); 121 | } 122 | 123 | @Test 124 | public void testNullQuery() throws Exception { 125 | assertEquals("?includefield=all",GoogleAPIClient.formatQuery(null)); 126 | } 127 | @Test 128 | public void testfuzzyMatching() throws Exception { 129 | StudyQuery query = new StudyQuery(); 130 | 131 | query.setFuzzyMatching(true); 132 | assertEquals("?includefield=all",GoogleAPIClient.formatQuery(query)); 133 | 134 | query.setPatientName("name"); 135 | query.setFuzzyMatching(true); 136 | assertEquals("?PatientName=name&fuzzymatching=true",GoogleAPIClient.formatQuery(query)); 137 | 138 | query.setPatientName("name"); 139 | query.setFuzzyMatching(false); 140 | assertEquals("?PatientName=name&fuzzymatching=false",GoogleAPIClient.formatQuery(query)); 141 | 142 | } 143 | 144 | 145 | @Test 146 | public void testPagination() throws Exception { 147 | StudyQuery query = new StudyQuery(); 148 | 149 | query.setPage(0); 150 | assertEquals("?includefield=all",GoogleAPIClient.formatQuery(query)); 151 | 152 | query.setPage(5); 153 | assertEquals("?includefield=all",GoogleAPIClient.formatQuery(query)); 154 | 155 | query.setPage(0); 156 | query.setPageSize(100); 157 | assertEquals("?limit=100&offset=0",GoogleAPIClient.formatQuery(query)); 158 | 159 | query.setPage(2); 160 | query.setPageSize(50); 161 | assertEquals("?limit=50&offset=100",GoogleAPIClient.formatQuery(query)); 162 | } 163 | 164 | @Test 165 | public void testShouldReturnProjectsWithNotNullNamesAndIds() throws Exception { 166 | // Given 167 | GoogleAPIClient client = PowerMockito.spy( 168 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 169 | Mockito.doReturn("TEST").when(client).signIn(); 170 | Field cloudResourceManagerField = GoogleAPIClient.class 171 | .getDeclaredField("cloudResourceManager"); 172 | cloudResourceManagerField.setAccessible(true); 173 | CloudResourceManager cloudResourceManager = mock(CloudResourceManager.class); 174 | cloudResourceManagerField.set(null, cloudResourceManager); 175 | Projects projectsMock = mock(Projects.class); 176 | when(cloudResourceManager.projects()).thenReturn(projectsMock); 177 | Projects.List listMock = mock(Projects.List.class); 178 | when(projectsMock.list()).thenReturn(listMock); 179 | ListProjectsResponse response = new ListProjectsResponse(); 180 | 181 | // First project 182 | Project project1 = new Project(); 183 | project1.setName("Project1"); 184 | project1.setProjectId("id1"); 185 | // Second with null name 186 | Project project2 = new Project(); 187 | project2.setName(null); 188 | project2.setProjectId("id2"); 189 | // Third with null projectId 190 | Project project3 = new Project(); 191 | project3.setName("Project3"); 192 | project3.setProjectId(null); 193 | List projects = new ArrayList<>(Arrays.asList(project1, project2, project3)); 194 | 195 | response.setProjects(projects); 196 | when(listMock.execute()).thenReturn(response); 197 | 198 | // When 199 | List allProjects = client.fetchProjects(); 200 | 201 | // Then 202 | assertEquals(1, allProjects.size()); 203 | } 204 | 205 | @Test(expected = Exception.class) 206 | public void testFetchDatasetsShouldReturnExceptionIfResponseContainZeroDatasets() 207 | throws Exception { 208 | // Given 209 | final GoogleAPIClient client = PowerMockito.spy( 210 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 211 | HttpTransport transport = new MockHttpTransport() { 212 | @Override 213 | public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 214 | return new MockLowLevelHttpRequest() { 215 | @Override 216 | public LowLevelHttpResponse execute() throws IOException { 217 | MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 218 | response.setStatusCode(200); 219 | response.setContentType(Json.MEDIA_TYPE); 220 | response.setContent("{}"); 221 | return response; 222 | } 223 | }; 224 | } 225 | }; 226 | HttpRequest request = transport.createRequestFactory() 227 | .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); 228 | HttpResponse response = request.execute(); 229 | PowerMockito.doReturn(response).when(client, "executeGetRequest", Mockito.any()); 230 | ProjectDescriptor projectDescriptor = new ProjectDescriptor("Test-1", "1"); 231 | Location location = new Location(projectDescriptor, "projects/1/locations/1", "1"); 232 | 233 | // Then 234 | client.fetchDatasets(location); 235 | } 236 | 237 | @Test 238 | public void testFetchDatasetsShouldReturnDatasetsIfResponseContainDatasets() throws Exception { 239 | // Given 240 | final GoogleAPIClient client = PowerMockito.spy( 241 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 242 | HttpTransport transport = new MockHttpTransport() { 243 | @Override 244 | public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 245 | return new MockLowLevelHttpRequest() { 246 | @Override 247 | public LowLevelHttpResponse execute() throws IOException { 248 | MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 249 | response.setStatusCode(200); 250 | response.setContentType(Json.MEDIA_TYPE); 251 | response.setContent("{\n" 252 | + " \"datasets\": [\n" 253 | + " {\n" 254 | + " \"name\": \"projects/1/locations/1/datasets/Test-1\"\n" 255 | + " },\n" 256 | + " {\n" 257 | + " \"name\": \"projects/1/locations/1/datasets/Test-2\"\n" 258 | + " }\n" 259 | + " ]\n" 260 | + "}"); 261 | return response; 262 | } 263 | }; 264 | } 265 | }; 266 | HttpRequest request = transport.createRequestFactory() 267 | .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); 268 | HttpResponse response = request.execute(); 269 | PowerMockito.doReturn(response).when(client, "executeGetRequest", Mockito.any()); 270 | ProjectDescriptor projectDescriptor = new ProjectDescriptor("Test-1", "1"); 271 | Location location = new Location(projectDescriptor, "projects/1/locations/1", "1"); 272 | 273 | // When 274 | List datasetList = client.fetchDatasets(location); 275 | 276 | // Then 277 | assertEquals(2, datasetList.size()); 278 | } 279 | 280 | @Test(expected = Exception.class) 281 | public void testFetchDicomstoresShouldReturnExceptionIfResponseContainZeroDicomstores() 282 | throws Exception { 283 | // Given 284 | final GoogleAPIClient client = PowerMockito.spy( 285 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 286 | HttpTransport transport = new MockHttpTransport() { 287 | @Override 288 | public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 289 | return new MockLowLevelHttpRequest() { 290 | @Override 291 | public LowLevelHttpResponse execute() throws IOException { 292 | MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 293 | response.setStatusCode(200); 294 | response.setContentType(Json.MEDIA_TYPE); 295 | response.setContent("{}"); 296 | return response; 297 | } 298 | }; 299 | } 300 | }; 301 | HttpRequest request = transport.createRequestFactory() 302 | .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); 303 | HttpResponse response = request.execute(); 304 | PowerMockito.doReturn(response).when(client, "executeGetRequest", Mockito.any()); 305 | ProjectDescriptor projectDescriptor = new ProjectDescriptor("Test-1", "1"); 306 | Location location = new Location(projectDescriptor, "projects/1/locations/1", "1"); 307 | Dataset dataset = new Dataset(location, "projects/1/locations/1/datasets/Test-1"); 308 | 309 | // Then 310 | client.fetchDicomstores(dataset); 311 | } 312 | 313 | @Test 314 | public void testFetchDicomstoresShouldReturnDicomstoresIfResponseContainDicomstores() 315 | throws Exception { 316 | // Given 317 | final GoogleAPIClient client = PowerMockito.spy( 318 | GoogleAPIClientFactory.getInstance().createGoogleClient()); 319 | HttpTransport transport = new MockHttpTransport() { 320 | @Override 321 | public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 322 | return new MockLowLevelHttpRequest() { 323 | @Override 324 | public LowLevelHttpResponse execute() throws IOException { 325 | MockLowLevelHttpResponse response = new MockLowLevelHttpResponse(); 326 | response.setStatusCode(200); 327 | response.setContentType(Json.MEDIA_TYPE); 328 | response.setContent("{\n" 329 | + " \"dicomStores\": [\n" 330 | + " {\n" 331 | + " \"name\": \"projects/1/locations/1/datasets/Test-1/dicomStores/Test-1\"\n" 332 | + " },\n" 333 | + " {\n" 334 | + " \"name\": \"projects/1/locations/1/datasets/Test-1/dicomStores/Test-2\"\n" 335 | + " }\n" 336 | + " ]\n" 337 | + "}"); 338 | return response; 339 | } 340 | }; 341 | } 342 | }; 343 | HttpRequest request = transport.createRequestFactory() 344 | .buildGetRequest(HttpTesting.SIMPLE_GENERIC_URL); 345 | HttpResponse response = request.execute(); 346 | PowerMockito.doReturn(response).when(client, "executeGetRequest", Mockito.any()); 347 | ProjectDescriptor projectDescriptor = new ProjectDescriptor("Test-1", "1"); 348 | Location location = new Location(projectDescriptor, "projects/1/locations/1", "1"); 349 | Dataset dataset = new Dataset(location, "projects/1/locations/1/datasets/Test-1"); 350 | 351 | // When 352 | List dicomStoreList = client.fetchDicomstores(dataset); 353 | 354 | // Then 355 | assertEquals(2, dicomStoreList.size()); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/GoogleAPIClient.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api; 16 | 17 | import com.fasterxml.jackson.core.type.TypeReference; 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import com.google.gson.JsonObject; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.nio.file.Paths; 23 | import org.weasis.dicom.google.api.model.Dataset; 24 | import org.weasis.dicom.google.api.model.DicomStore; 25 | import org.weasis.dicom.google.api.model.Location; 26 | import org.weasis.dicom.google.api.model.ProjectDescriptor; 27 | import org.weasis.dicom.google.api.model.StudyModel; 28 | import org.weasis.dicom.google.api.model.StudyQuery; 29 | import org.weasis.dicom.google.api.ui.OAuth2Browser; 30 | import com.google.api.client.auth.oauth2.Credential; 31 | import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp; 32 | import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver; 33 | import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow; 34 | import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; 35 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; 36 | import com.google.api.client.http.GenericUrl; 37 | import com.google.api.client.http.HttpHeaders; 38 | import com.google.api.client.http.HttpRequest; 39 | import com.google.api.client.http.HttpResponse; 40 | import com.google.api.client.http.HttpResponseException; 41 | import com.google.api.client.http.HttpStatusCodes; 42 | import com.google.api.client.http.HttpTransport; 43 | import com.google.api.client.json.JsonFactory; 44 | import com.google.api.client.json.jackson2.JacksonFactory; 45 | import com.google.api.client.util.store.DataStoreFactory; 46 | import com.google.api.client.util.store.FileDataStoreFactory; 47 | import com.google.api.services.cloudresourcemanager.CloudResourceManager; 48 | import com.google.api.services.cloudresourcemanager.model.ListProjectsResponse; 49 | import com.google.api.services.cloudresourcemanager.model.Project; 50 | import com.google.api.services.oauth2.Oauth2; 51 | import com.google.api.services.oauth2.model.Tokeninfo; 52 | import com.google.gson.JsonArray; 53 | import com.google.gson.JsonElement; 54 | import com.google.gson.JsonParser; 55 | 56 | import java.io.*; 57 | import java.security.GeneralSecurityException; 58 | import java.time.format.DateTimeFormatter; 59 | import java.util.ArrayList; 60 | import java.util.Arrays; 61 | import java.util.List; 62 | import java.util.Objects; 63 | import java.util.stream.Collectors; 64 | import java.util.stream.StreamSupport; 65 | 66 | import static org.weasis.dicom.google.api.util.StringUtils.*; 67 | 68 | public class GoogleAPIClient { 69 | 70 | private static final String APPLICATION_NAME = "Weasis-GoogleDICOMExplorer/1.0"; 71 | 72 | /** 73 | * Directory to store user credentials. 74 | */ 75 | private static final java.io.File DATA_STORE_DIR = new java.io.File(System.getProperty("user.home"), ".weasis/google_auth"); 76 | private static final String GOOGLE_API_BASE_PATH = "https://healthcare.googleapis.com/v1beta1"; 77 | private static final String SECRETS_FILE_NAME = "client_secrets.json"; 78 | /** 79 | * Global instance of the {@link DataStoreFactory}. The best practice is to make 80 | * it a single globally shared instance across your application. 81 | */ 82 | private static FileDataStoreFactory dataStoreFactory; 83 | 84 | /** 85 | * Global instance of the HTTP transport. 86 | */ 87 | private static HttpTransport httpTransport; 88 | 89 | /** 90 | * Global instance of the JSON factory. 91 | */ 92 | private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); 93 | 94 | /** 95 | * OAuth 2.0 scopes. 96 | */ 97 | private static final List SCOPES = Arrays.asList( 98 | "https://www.googleapis.com/auth/cloud-healthcare", 99 | "https://www.googleapis.com/auth/cloudplatformprojects.readonly" 100 | ); 101 | 102 | private static Oauth2 oauth2; 103 | private static GoogleClientSecrets clientSecrets; 104 | 105 | private final ObjectMapper objectMapper = new ObjectMapper(); 106 | private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); 107 | /** 108 | * Instance of Google Cloud Resource Manager 109 | */ 110 | private static CloudResourceManager cloudResourceManager; 111 | 112 | private boolean isSignedIn = false; 113 | private String accessToken; 114 | 115 | protected GoogleAPIClient() { 116 | } 117 | 118 | private static GoogleAuthorizationCodeFlow authorizationCodeFlow() throws GeneralSecurityException, IOException { 119 | httpTransport = GoogleNetHttpTransport.newTrustedTransport(); 120 | dataStoreFactory = new FileDataStoreFactory(DATA_STORE_DIR); 121 | 122 | try (InputStream in = getSecret()) { 123 | clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in)); 124 | } 125 | if (clientSecrets.getDetails().getClientId().startsWith("Enter") 126 | || clientSecrets.getDetails().getClientSecret().startsWith("Enter ")) { 127 | throw new RuntimeException(SECRETS_FILE_NAME + " not found"); 128 | } 129 | return new GoogleAuthorizationCodeFlow.Builder(httpTransport, JSON_FACTORY, 130 | clientSecrets, SCOPES).setDataStoreFactory(dataStoreFactory).build(); 131 | } 132 | 133 | private static Credential authorize() throws Exception { 134 | // set up authorization code flow 135 | GoogleAuthorizationCodeFlow flow = authorizationCodeFlow(); 136 | // authorize 137 | return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver(), 138 | OAuth2Browser.INSTANCE).authorize("user"); 139 | } 140 | 141 | private static InputStream getSecret() throws IOException { 142 | String extendedProperties = System.getProperty("felix.extended.config.properties") 143 | .replace("file:", ""); 144 | Path extendedPropertiesFolderPath = Paths.get(extendedProperties).getParent(); 145 | Path clientSecretsFile = extendedPropertiesFolderPath.resolve(SECRETS_FILE_NAME); 146 | if (Files.exists(clientSecretsFile)) { 147 | System.out.println("Loading user's client_secret.json"); 148 | return Files.newInputStream(clientSecretsFile); 149 | } else { 150 | System.out.println("Loading embedded client_secret.json"); 151 | return GoogleAPIClient.class.getResource("/" + SECRETS_FILE_NAME).openStream(); 152 | } 153 | } 154 | 155 | public String getAccessToken() { 156 | if (accessToken == null) { 157 | isSignedIn = false; 158 | signIn(); 159 | } 160 | return accessToken; 161 | } 162 | 163 | public String signIn() { 164 | if (!isSignedIn) { 165 | int tryCount = 0; 166 | Exception error; 167 | do { 168 | try { 169 | tryCount++; 170 | // authorization 171 | Credential credential = authorize(); 172 | // set up global Oauth2 instance 173 | oauth2 = new Oauth2.Builder(httpTransport, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME) 174 | .build(); 175 | 176 | cloudResourceManager = new CloudResourceManager.Builder(httpTransport, JSON_FACTORY, credential) 177 | .build(); 178 | accessToken = credential.getAccessToken(); 179 | // run commands 180 | tokenInfo(accessToken); 181 | error = null; 182 | isSignedIn = true; 183 | } catch (Exception e) { 184 | error = e; 185 | } 186 | } while (!isSignedIn && tryCount < 4); 187 | if (error != null) { 188 | throw new IllegalStateException(error); 189 | } 190 | } 191 | return accessToken; 192 | } 193 | 194 | public void signOut() { 195 | clearSignIn(); 196 | isSignedIn = false; 197 | } 198 | 199 | public String refresh() { 200 | if (isSignedIn) { 201 | isSignedIn = false; 202 | return signIn(); 203 | } 204 | return getAccessToken(); 205 | } 206 | 207 | private void clearSignIn() { 208 | accessToken = null; 209 | deleteDir(DATA_STORE_DIR); 210 | } 211 | 212 | public boolean isAuthorized() { 213 | boolean isAuthorized; 214 | try { 215 | GoogleAuthorizationCodeFlow flow = authorizationCodeFlow(); 216 | isAuthorized = Objects.nonNull(flow.loadCredential("user")); 217 | } catch (IOException| GeneralSecurityException e) { 218 | isAuthorized = false; 219 | } 220 | return isAuthorized; 221 | } 222 | 223 | private void deleteDir(File file) { 224 | if (!file.exists()) { 225 | return; 226 | } 227 | if (file.isDirectory()) { 228 | for (File child : file.listFiles()) { 229 | deleteDir(child); 230 | } 231 | } 232 | file.delete(); 233 | } 234 | 235 | private static void tokenInfo(String accessToken) throws IOException { 236 | Tokeninfo tokeninfo = oauth2.tokeninfo().setAccessToken(accessToken).execute(); 237 | if (!tokeninfo.getAudience().equals(clientSecrets.getDetails().getClientId())) { 238 | System.err.println("ERROR: audience does not match our client ID!"); 239 | } 240 | } 241 | 242 | public List fetchProjects() throws Exception { 243 | refresh(); 244 | List result = new ArrayList(); 245 | CloudResourceManager.Projects.List request = cloudResourceManager.projects().list(); 246 | ListProjectsResponse response; 247 | do { 248 | response = request.execute(); 249 | if (response.getProjects() == null) { 250 | continue; 251 | } 252 | for (Project project : response.getProjects()) { 253 | String name = project.getName(); 254 | String projectId = project.getProjectId(); 255 | if (name != null && projectId != null) { 256 | result.add(new org.weasis.dicom.google.api.model.ProjectDescriptor(name, projectId)); 257 | } 258 | } 259 | request.setPageToken(response.getNextPageToken()); 260 | } while (response.getNextPageToken() != null); 261 | return result; 262 | } 263 | 264 | private String parseName(String name) { 265 | return name.substring(name.lastIndexOf("/") + 1); 266 | } 267 | 268 | /** 269 | * Executes HTTP GET request using the specified URL. 270 | * 271 | * @param url HTTP request URL. 272 | * @return HTTP response. 273 | * @throws IOException if an IO error occurred. 274 | * @throws HttpResponseException if an error status code is detected in response. 275 | * @see #executeGetRequest(String, HttpHeaders) 276 | */ 277 | public HttpResponse executeGetRequest(String url) throws IOException { 278 | return executeGetRequest(url, new HttpHeaders()); 279 | } 280 | 281 | /** 282 | * Executes a HTTP GET request with the specified URL and headers. GCP authorization is done 283 | * if the user is not already signed in. The access token is refreshed if it has expired 284 | * (HTTP 401 is returned from the server) and the request is retried with the new access token. 285 | * 286 | * @param url HTTP request URL. 287 | * @param headers HTTP request headers. 288 | * @return HTTP response. 289 | * @throws IOException if an IO error occurred. 290 | * @throws HttpResponseException if an error status code is detected in response. 291 | * @see #signIn() 292 | * @see #refresh() 293 | */ 294 | public HttpResponse executeGetRequest(String url, HttpHeaders headers) throws IOException { 295 | signIn(); 296 | try { 297 | return doExecuteGetRequest(url, headers); 298 | } catch (HttpResponseException e) { 299 | // Token expired? 300 | if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_UNAUTHORIZED) { 301 | // Refresh token and try again 302 | refresh(); 303 | return doExecuteGetRequest(url, headers); 304 | } 305 | throw e; 306 | } 307 | } 308 | 309 | /** 310 | * Performs actual HTTP GET request using the specified URL and headers. This method also adds 311 | * {@code Authorization} header initialized with OAuth access token. 312 | * 313 | * @param url HTTP request URL. 314 | * @param headers HTTP request headers. 315 | * @return HTTP response. 316 | * @throws IOException if an IO error occurred. 317 | * @throws HttpResponseException if an error status code is detected in response. 318 | * @see #doExecuteGetRequest(String, HttpHeaders) 319 | */ 320 | private HttpResponse doExecuteGetRequest(String url, HttpHeaders headers) throws IOException { 321 | final HttpRequest request = httpTransport.createRequestFactory().buildGetRequest( 322 | new GenericUrl(url)); 323 | headers.setAuthorization("Bearer " + accessToken); 324 | request.setHeaders(headers); 325 | return request.execute(); 326 | } 327 | 328 | public List fetchLocations(ProjectDescriptor project) throws Exception { 329 | String url = GOOGLE_API_BASE_PATH + "/projects/" + project.getId() + "/locations"; 330 | String data = executeGetRequest(url).parseAsString(); 331 | JsonParser parser = new JsonParser(); 332 | JsonElement jsonTree = parser.parse(data); 333 | JsonArray jsonObject = jsonTree.getAsJsonObject().get("locations").getAsJsonArray(); 334 | return StreamSupport.stream(jsonObject.spliterator(), false) 335 | .map(JsonElement::getAsJsonObject) 336 | .map(obj -> new org.weasis.dicom.google.api.model.Location(project, 337 | obj.get("name").getAsString(), 338 | obj.get("locationId").getAsString())) 339 | .collect(Collectors.toList()); 340 | } 341 | 342 | public List fetchDatasets(Location location) throws Exception { 343 | String locationId = location.getId(); 344 | String url = GOOGLE_API_BASE_PATH + "/projects/" + location.getParent().getId() + "/locations/" + locationId + "/datasets"; 345 | String data = executeGetRequest(url).parseAsString(); 346 | JsonParser parser = new JsonParser(); 347 | JsonElement jsonTree = parser.parse(data); 348 | if (((JsonObject) jsonTree).size() == 0) { 349 | throw new Exception("No Datasets in " + locationId + " location"); 350 | } 351 | JsonArray jsonObject = jsonTree.getAsJsonObject().get("datasets").getAsJsonArray(); 352 | return StreamSupport.stream(jsonObject.spliterator(), false) 353 | .map(obj -> obj.getAsJsonObject().get("name").getAsString()) 354 | .map(this::parseName) 355 | .map(name -> new Dataset(location, name)) 356 | .collect(Collectors.toList()); 357 | } 358 | 359 | public List fetchDicomstores(Dataset dataset) throws Exception { 360 | String datasetName = dataset.getName(); 361 | String url = GOOGLE_API_BASE_PATH 362 | + "/projects/" + dataset.getProject().getId() 363 | + "/locations/" + dataset.getParent().getId() 364 | + "/datasets/" + datasetName + "/dicomStores"; 365 | String data = executeGetRequest(url).parseAsString(); 366 | JsonParser parser = new JsonParser(); 367 | JsonElement jsonTree = parser.parse(data); 368 | if (((JsonObject) jsonTree).size() == 0) { 369 | throw new Exception("No DICOM Stores in " + datasetName + " Dataset"); 370 | } 371 | JsonArray jsonObject = jsonTree.getAsJsonObject().get("dicomStores").getAsJsonArray(); 372 | 373 | return StreamSupport.stream(jsonObject.spliterator(), false) 374 | .map(obj -> obj.getAsJsonObject().get("name").getAsString()) 375 | .map(this::parseName) 376 | .map(name -> new DicomStore(dataset, name)) 377 | .collect(Collectors.toList()); 378 | } 379 | 380 | public List fetchStudies(DicomStore store, StudyQuery query) throws Exception { 381 | String url = GOOGLE_API_BASE_PATH 382 | + "/projects/" + store.getProject().getId() 383 | + "/locations/" + store.getLocation().getId() 384 | + "/datasets/" + store.getParent().getName() 385 | + "/dicomStores/" + store.getName() 386 | + "/dicomWeb/studies" + formatQuery(query); 387 | String data = executeGetRequest(url).parseAsString(); 388 | List studies = objectMapper.readValue(data, new TypeReference>() { 389 | }); 390 | 391 | return studies; 392 | } 393 | 394 | public static String getImageUrl(DicomStore store, String studyId) { 395 | return GOOGLE_API_BASE_PATH 396 | + "/projects/" + store.getProject().getId() 397 | + "/locations/" + store.getLocation().getId() 398 | + "/datasets/" + store.getParent().getName() 399 | + "/dicomStores/" + store.getName() 400 | + "/dicomWeb/studies/" + studyId; 401 | } 402 | 403 | /** 404 | * Generate String with GET variables for study request url 405 | * 406 | * @param query source of data for GET variables 407 | * @return GET variables 408 | * @see StudyQuery 409 | */ 410 | public static String formatQuery(StudyQuery query) { 411 | String allItems = "?includefield=all"; 412 | if (Objects.isNull(query)) { 413 | return allItems; 414 | } 415 | 416 | List parameters = new ArrayList<>(); 417 | if (isNotBlank(query.getPatientName())) { 418 | parameters.add("PatientName=" + urlEncode(query.getPatientName())); 419 | parameters.add("fuzzymatching=" + (query.getFuzzyMatching() ? "true" : "false")); 420 | } 421 | 422 | if (isNotBlank(query.getPatientId())) { 423 | parameters.add("PatientID=" + urlEncode(query.getPatientId())); 424 | } 425 | 426 | if (isNotBlank(query.getAccessionNumber())) { 427 | parameters.add("AccessionNumber=" + urlEncode(query.getAccessionNumber())); 428 | } 429 | 430 | if (query.getStartDate() != null && query.getEndDate() != null) { 431 | parameters.add("StudyDate=" + urlEncode(DATE_FORMAT.format(query.getStartDate())) + "-" 432 | + urlEncode(DATE_FORMAT.format(query.getEndDate()))); 433 | } 434 | 435 | int pageNumber = query.getPage(); 436 | int pageSize = query.getPageSize(); 437 | 438 | if (pageSize > 0) { 439 | parameters.add("limit=" + String.valueOf(pageSize)); 440 | 441 | if (pageNumber >= 0) { 442 | parameters.add("offset=" + String.valueOf(pageNumber * pageSize)); 443 | } 444 | } 445 | 446 | if (isNotBlank(query.getPhysicianName())) { 447 | parameters.add("ReferringPhysicianName=" + urlEncode(query.getPhysicianName())); 448 | } 449 | 450 | if (parameters.isEmpty()) { 451 | return allItems; 452 | } else { 453 | return "?" + join(parameters, "&"); 454 | } 455 | } 456 | 457 | } 458 | 459 | -------------------------------------------------------------------------------- /src/main/java/org/weasis/dicom/google/api/ui/dicomstore/DicomStoreSelector.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package org.weasis.dicom.google.api.ui.dicomstore; 16 | 17 | import org.weasis.dicom.google.api.GoogleAPIClient; 18 | import org.weasis.dicom.google.api.model.Dataset; 19 | import org.weasis.dicom.google.api.model.DicomStore; 20 | import org.weasis.dicom.google.api.model.Location; 21 | import org.weasis.dicom.google.api.model.ProjectDescriptor; 22 | import org.weasis.dicom.google.api.model.StudyQuery; 23 | import org.weasis.dicom.google.api.ui.StudiesTable; 24 | import org.weasis.dicom.google.api.ui.StudyView; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | import org.weasis.dicom.google.explorer.Messages; 28 | 29 | import javax.swing.Box; 30 | import javax.swing.BoxLayout; 31 | import javax.swing.DefaultComboBoxModel; 32 | import javax.swing.DefaultListCellRenderer; 33 | import javax.swing.JButton; 34 | import javax.swing.JPanel; 35 | import javax.swing.JComboBox; 36 | import javax.swing.JTextField; 37 | import javax.swing.JList; 38 | import javax.swing.ListCellRenderer; 39 | import javax.swing.SwingWorker; 40 | import javax.swing.SwingUtilities; 41 | import javax.swing.UIManager; 42 | import javax.swing.plaf.basic.BasicComboBoxEditor; 43 | import javax.swing.plaf.basic.BasicComboPopup; 44 | 45 | import java.awt.Component; 46 | import java.awt.Dimension; 47 | import java.awt.Window; 48 | import java.awt.event.ItemEvent; 49 | import java.awt.event.ItemListener; 50 | import java.awt.event.KeyAdapter; 51 | import java.awt.event.KeyEvent; 52 | import java.awt.event.MouseAdapter; 53 | import java.awt.event.MouseEvent; 54 | import java.util.ArrayList; 55 | import java.util.List; 56 | import java.util.Objects; 57 | import java.util.Optional; 58 | import java.util.function.Consumer; 59 | import java.util.function.Function; 60 | 61 | import static java.util.Collections.emptyList; 62 | 63 | public class DicomStoreSelector extends JPanel { 64 | 65 | private static final Logger log = LoggerFactory.getLogger(DicomStoreSelector.class); 66 | 67 | private static final String TEXT_GOOGLE_SIGN_IN = Messages.getString("DicomStoreSelector.sign_in"); //$NON-NLS-1$ 68 | private static final String DEFAULT_PROJECT_COMBOBOX_TEXT = Messages.getString("DicomStoreSelector.default_project_text"); //$NON-NLS-1$ 69 | private static final String DEFAULT_LOCATION_COMBOBOX_TEXT = Messages.getString("DicomStoreSelector.default_location_text"); //$NON-NLS-1$ 70 | private static final String DEFAULT_DATASET_COMBOBOX_TEXT = Messages.getString("DicomStoreSelector.default_dataset_text"); //$NON-NLS-1$ 71 | private static final String DEFAULT_DICOMSTORE_COMBOBOX_TEXT = Messages.getString("DicomStoreSelector.default_dicomstore_text"); //$NON-NLS-1$ 72 | private static final String ACTION_SIGN_IN = Messages.getString("DicomStoreSelector.sign_in"); 73 | 74 | private final GoogleAPIClient googleAPIClient; 75 | 76 | private final DefaultComboBoxModel> modelProject = new DefaultComboBoxModel<>(); 77 | private final DefaultComboBoxModel> modelLocation = new DefaultComboBoxModel<>(); 78 | private final DefaultComboBoxModel> modelDataset = new DefaultComboBoxModel<>(); 79 | private final DefaultComboBoxModel> modelDicomstore = new DefaultComboBoxModel<>(); 80 | 81 | private final StudiesTable table; 82 | private List projects; 83 | private final JProjectComboBox> googleProjectCombobox = new JProjectComboBox<>(modelProject); 84 | private final JComboBox> googleLocationCombobox = new JComboBox<>(modelLocation); 85 | private final JComboBox> googleDatasetCombobox = new JComboBox<>(modelDataset); 86 | private final JComboBox> googleDicomstoreCombobox = new JComboBox<>(modelDicomstore); 87 | private final JButton googleAuthButton = new JButton(); 88 | 89 | 90 | private void processSignedIn(JButton googleAuthButton) { 91 | new GoogleLoginTask(googleAPIClient, googleAuthButton, this).execute(); 92 | } 93 | 94 | private void toLogoutState(){ 95 | googleAuthButton.setText(TEXT_GOOGLE_SIGN_IN); 96 | googleAuthButton.setActionCommand(ACTION_SIGN_IN); 97 | googleProjectCombobox.setEnabled(false); 98 | googleLocationCombobox.setEnabled(false); 99 | googleDatasetCombobox.setEnabled(false); 100 | googleDicomstoreCombobox.setEnabled(false); 101 | modelProject.removeAllElements(); 102 | modelLocation.removeAllElements(); 103 | modelDataset.removeAllElements(); 104 | modelDicomstore.removeAllElements(); 105 | table.clearTable(); 106 | JTextField textField = (JTextField) googleProjectCombobox.getEditor().getEditorComponent(); 107 | textField.setText(""); 108 | } 109 | 110 | public DicomStoreSelector(GoogleAPIClient googleAPIClient, StudiesTable table) { 111 | UIManager.getLookAndFeelDefaults().put("ComboBox.noActionOnKeyNavigation", true); 112 | this.googleAPIClient = googleAPIClient; 113 | this.table = table; 114 | BoxLayout layout = new BoxLayout(this, BoxLayout.X_AXIS); 115 | setLayout(layout); 116 | 117 | googleProjectCombobox.setPrototypeDisplayValue(Optional.empty()); 118 | googleLocationCombobox.setPrototypeDisplayValue(Optional.empty()); 119 | googleDatasetCombobox.setPrototypeDisplayValue(Optional.empty()); 120 | googleDicomstoreCombobox.setPrototypeDisplayValue(Optional.empty()); 121 | 122 | if (googleAPIClient.isAuthorized()) { 123 | processSignedIn(googleAuthButton); 124 | } else { 125 | toLogoutState(); 126 | } 127 | googleAuthButton.addActionListener(e -> { 128 | if (!googleAPIClient.isAuthorized()) { 129 | processSignedIn(googleAuthButton); 130 | } else { 131 | googleAPIClient.signOut(); 132 | toLogoutState(); 133 | } 134 | }); 135 | 136 | add(googleProjectCombobox); 137 | add(Box.createHorizontalStrut(10)); 138 | add(googleLocationCombobox); 139 | add(Box.createHorizontalStrut(10)); 140 | add(googleDatasetCombobox); 141 | add(Box.createHorizontalStrut(10)); 142 | add(googleDicomstoreCombobox); 143 | add(Box.createHorizontalStrut(10)); 144 | add(googleAuthButton); 145 | add(Box.createHorizontalGlue()); 146 | 147 | googleProjectCombobox.setRenderer(new ListRenderer<>(ProjectDescriptor::getName, DEFAULT_PROJECT_COMBOBOX_TEXT)); 148 | googleLocationCombobox.setRenderer(new ListRenderer<>(Location::getId, DEFAULT_LOCATION_COMBOBOX_TEXT)); 149 | googleDatasetCombobox.setRenderer(new ListRenderer<>(Dataset::getName, DEFAULT_DATASET_COMBOBOX_TEXT)); 150 | googleDicomstoreCombobox.setRenderer(new ListRenderer<>(DicomStore::getName, DEFAULT_DICOMSTORE_COMBOBOX_TEXT)); 151 | googleProjectCombobox.setLightWeightPopupEnabled(false); 152 | 153 | googleProjectCombobox.addItemListener(this.selectedListener( 154 | project -> new LoadLocationsTask(project, googleAPIClient, this), 155 | nothing -> updateLocations(emptyList()) 156 | )); 157 | 158 | AutoRefreshComboBoxExtension.wrap(googleProjectCombobox, () -> { 159 | log.info("Reloading projects"); 160 | new LoadProjectsTask(googleAPIClient, DicomStoreSelector.this).execute(); 161 | return true; 162 | }); 163 | 164 | googleProjectCombobox.setEditor(new JProjectComboBoxEditor(DEFAULT_PROJECT_COMBOBOX_TEXT)); 165 | googleProjectCombobox.getEditor().getEditorComponent().addMouseListener(new MouseAdapter() { 166 | @Override 167 | public void mousePressed(MouseEvent e) { 168 | if (googleProjectCombobox.firstFocusGain) { 169 | ((JTextField) googleProjectCombobox.getEditor().getEditorComponent()).setText(""); 170 | googleProjectCombobox.firstFocusGain = false; 171 | } 172 | super.mousePressed(e); 173 | } 174 | }); 175 | googleProjectCombobox.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() { 176 | @Override 177 | public void keyReleased(KeyEvent arg0) { 178 | JTextField textField = (JTextField) googleProjectCombobox.getEditor().getEditorComponent(); 179 | if (googleProjectCombobox.firstFocusGain) { 180 | textField.setText(textField.getText().replaceAll(DEFAULT_PROJECT_COMBOBOX_TEXT, "")); 181 | googleProjectCombobox.firstFocusGain = false; 182 | } 183 | googleProjectCombobox.search(textField.getText()); 184 | } 185 | }); 186 | 187 | googleLocationCombobox.addItemListener(this.selectedListener( 188 | location -> new LoadDatasetsTask(location, googleAPIClient, this), 189 | nothing -> updateDatasets(emptyList()) 190 | )); 191 | 192 | AutoRefreshComboBoxExtension.wrap(googleLocationCombobox, () -> 193 | getSelectedItem(modelProject).map( 194 | (project) -> { 195 | log.info("Reloading locations"); 196 | new LoadLocationsTask(project, googleAPIClient, DicomStoreSelector.this).execute(); 197 | return true; 198 | } 199 | ).orElse(false) 200 | ); 201 | 202 | googleDatasetCombobox.addItemListener(this.selectedListener( 203 | dataset -> new LoadDicomStoresTask(dataset, googleAPIClient, this), 204 | nothing -> updateDicomStores(emptyList()) 205 | )); 206 | 207 | AutoRefreshComboBoxExtension.wrap(googleDatasetCombobox, () -> 208 | getSelectedItem(modelLocation).map( 209 | (location) -> { 210 | log.info("Reloading Datasets"); 211 | new LoadDatasetsTask(location, googleAPIClient, DicomStoreSelector.this).execute(); 212 | return true; 213 | } 214 | ).orElse(false) 215 | ); 216 | googleDicomstoreCombobox.addItemListener(item -> 217 | emitStoreUpdateUpdate(new StoreUpdateEvent(this))); 218 | 219 | AutoRefreshComboBoxExtension.wrap(googleDicomstoreCombobox, () -> 220 | getSelectedItem(modelDataset).map( 221 | (dataset) -> { 222 | log.info("Reloading Dicom stores"); 223 | new LoadDicomStoresTask(dataset, googleAPIClient, DicomStoreSelector.this).execute(); 224 | return true; 225 | } 226 | ).orElse(false) 227 | ); 228 | } 229 | 230 | private LoadStudiesTask loadStudiesTask(DicomStore store, StudyQuery query) { 231 | return new LoadStudiesTask(store, googleAPIClient, this, query); 232 | } 233 | 234 | public void updateProjects(List result) { 235 | googleProjectCombobox.setEnabled(true); 236 | projects = result; 237 | if (updateModel(result, modelProject)) { 238 | googleProjectCombobox.firstFocusGain = true; 239 | JTextField textField = (JTextField) googleProjectCombobox.getEditor().getEditorComponent(); 240 | textField.setText(DEFAULT_PROJECT_COMBOBOX_TEXT); 241 | updateLocations(emptyList()); 242 | } 243 | } 244 | 245 | public void updateLocations(List result) { 246 | googleLocationCombobox.setEnabled(true); 247 | if (updateModel(result, modelLocation)) { 248 | updateDatasets(emptyList()); 249 | } 250 | } 251 | 252 | public void updateDatasets(List result) { 253 | googleDatasetCombobox.setEnabled(true); 254 | if (updateModel(result, modelDataset)) { 255 | updateDicomStores(emptyList()); 256 | } 257 | } 258 | 259 | public void updateDicomStores(List result) { 260 | googleDicomstoreCombobox.setEnabled(true); 261 | if (updateModel(result, modelDicomstore)) { 262 | updateTable(emptyList()); 263 | } 264 | } 265 | 266 | public void updateTable(List studies) { 267 | table.clearTable(); 268 | studies.forEach(table::addStudy); 269 | } 270 | 271 | public Optional getCurrentStore() { 272 | Object store = modelDicomstore.getSelectedItem(); 273 | Optional storeOptional = Optional.empty(); 274 | if (Objects.nonNull(store)) { 275 | storeOptional = (Optional) modelDicomstore.getSelectedItem(); 276 | } 277 | return storeOptional; 278 | } 279 | 280 | /** 281 | * @return true if selected item changed, false otherwise 282 | */ 283 | private boolean updateModel(List list, DefaultComboBoxModel> model) { 284 | Optional selectedItem = (Optional) model.getSelectedItem(); 285 | return Optional.ofNullable(selectedItem) 286 | .flatMap(x -> x) 287 | .filter(list::contains) 288 | .map(item -> { 289 | replaceAllExcludingItem(item, list, model); 290 | return false; 291 | }) 292 | .orElseGet(() -> { 293 | model.removeAllElements(); 294 | if (!list.isEmpty()) { 295 | model.addElement(Optional.empty()); 296 | list.stream().map(Optional::of).forEach(model::addElement); 297 | model.setSelectedItem(Optional.empty()); 298 | } 299 | return true; 300 | }); 301 | } 302 | 303 | private void replaceAllExcludingItem(T selectedItem, List list, DefaultComboBoxModel> model) { 304 | List> toDelete = new ArrayList<>(); 305 | for (int i = 0; i < model.getSize(); i++) { 306 | Optional currentItem = model.getElementAt(i); 307 | if (!Objects.equals(currentItem, Optional.of(selectedItem))) { 308 | toDelete.add(currentItem); 309 | } 310 | } 311 | toDelete.forEach(model::removeElement); 312 | 313 | int selectedIndex = list.indexOf(selectedItem); 314 | model.insertElementAt(Optional.empty(), 0); 315 | for (int i = 0; i < list.size(); i++) { 316 | if (selectedIndex != i) { 317 | if (selectedIndex > i) { 318 | model.insertElementAt(Optional.of(list.get(i)), model.getSize() - 1); 319 | } else { 320 | model.insertElementAt(Optional.of(list.get(i)), model.getSize()); 321 | } 322 | } 323 | } 324 | 325 | } 326 | 327 | private ItemListener selectedListener(Function> taskFactory, Consumer onEmpty) { 328 | return e -> { 329 | if (e.getStateChange() == ItemEvent.SELECTED && e.getItem().getClass().equals(Optional.class)) { 330 | Optional item = (Optional) e.getItem(); 331 | 332 | if (!item.isPresent()) { 333 | onEmpty.accept(null); 334 | } 335 | 336 | item.map(taskFactory).ifPresent(SwingWorker::execute); 337 | } 338 | }; 339 | } 340 | 341 | private static Optional getSelectedItem(DefaultComboBoxModel> model) { 342 | return Optional.ofNullable(model.getSelectedItem()) 343 | .flatMap(x -> (Optional) x); 344 | } 345 | 346 | /** Loads all DICOM studies that match the query 347 | * @param query parameters for study request 348 | */ 349 | public void loadStudies(StudyQuery query) { 350 | getCurrentStore().ifPresent((store) -> { 351 | loadStudiesTask(store, query).execute(); 352 | }); 353 | } 354 | 355 | /** Notify all store update listeners with update event 356 | * @param storeUpdateEvent event to pass 357 | */ 358 | public void emitStoreUpdateUpdate(StoreUpdateEvent storeUpdateEvent) { 359 | Object[] listeners = listenerList.getListenerList(); 360 | for (int i = 0; i < listeners.length; i = i + 2) { 361 | if (listeners[i] == StoreUpdateListener.class) { 362 | ((StoreUpdateListener) listeners[i + 1]).actionPerformed(storeUpdateEvent); 363 | } 364 | } 365 | } 366 | 367 | /** Adds StoreUpdateListener listener to common listeners list 368 | * @param listener listener to add 369 | */ 370 | public void addStoreListener(StoreUpdateListener listener) { 371 | this.listenerList.add(StoreUpdateListener.class, listener); 372 | } 373 | 374 | private class ListRenderer implements ListCellRenderer> { 375 | 376 | private final DefaultListCellRenderer renderer = new DefaultListCellRenderer(); 377 | private final String defaultLabel; 378 | private final Function textExtractor; 379 | 380 | public ListRenderer(Function textExtractor, String defaultLabel) { 381 | this.textExtractor = textExtractor; 382 | this.defaultLabel = defaultLabel; 383 | } 384 | 385 | @Override 386 | public Component getListCellRendererComponent(JList> list, 387 | Optional value, 388 | int index, 389 | boolean isSelected, 390 | boolean cellHasFocus) { 391 | renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 392 | if (value != null) { 393 | String label = value.map(textExtractor).orElse(defaultLabel); 394 | renderer.setText(label); 395 | } 396 | return renderer; 397 | } 398 | } 399 | 400 | private class JProjectComboBoxEditor extends BasicComboBoxEditor { 401 | public JProjectComboBoxEditor(String defaultText) { 402 | editor.setText(defaultText); 403 | } 404 | 405 | public void setItem(Object anObject) { 406 | if (anObject != null) { 407 | Optional item = anObject.getClass().equals(String.class) ? Optional.empty() : (Optional) anObject; 408 | if (item.isPresent()) { 409 | editor.setText(item.get().getName()); 410 | } 411 | } 412 | } 413 | } 414 | 415 | private class JProjectComboBox extends JComboBox { 416 | 417 | private static final long serialVersionUID = 450383631220222610L; 418 | private boolean firstFocusGain = true; 419 | private String prevInput = ""; 420 | 421 | public JProjectComboBox(DefaultComboBoxModel model) { 422 | super(model); 423 | } 424 | 425 | public void search(String input) { 426 | if ((input == null && prevInput == null) || (input.toLowerCase().equals(prevInput.toLowerCase()))) { 427 | return; 428 | } 429 | removeAllItems(); 430 | List updated = new ArrayList<>(); 431 | for (int i = 0; i < projects.size(); i++) { 432 | ProjectDescriptor item = projects.get(i); 433 | if (item.getName().toLowerCase().contains(input.toLowerCase())) { 434 | updated.add(item); 435 | } 436 | } 437 | prevInput = input; 438 | updateModel(updated, modelProject); 439 | if (updated.isEmpty() || (updated.size() == 1 && updated.get(0).equals(input))) { 440 | this.setPopupVisible(false); 441 | return; 442 | } 443 | this.setPopupVisible(true); 444 | BasicComboPopup popup = (BasicComboPopup) this.getAccessibleContext().getAccessibleChild(0); 445 | Window popupWindow = SwingUtilities.windowForComponent(popup); 446 | Window comboWindow = SwingUtilities.windowForComponent(this); 447 | 448 | if (comboWindow.equals(popupWindow)) { 449 | Component c = popup.getParent(); 450 | Dimension d = c.getPreferredSize(); 451 | c.setSize(d); 452 | } else { 453 | popupWindow.pack(); 454 | } 455 | } 456 | } 457 | 458 | } 459 | --------------------------------------------------------------------------------