diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d11829c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.classpath +.project +.settings/ +target/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..721ea11 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +# Changelog for Catalogue Core Library + +## [v1.0.0-SNAPSHOT] + +- First Version + diff --git a/FUNDING.md b/FUNDING.md new file mode 100644 index 0000000..9e48b94 --- /dev/null +++ b/FUNDING.md @@ -0,0 +1,26 @@ +# Acknowledgments + +The projects leading to this software have received funding from a series of European Union programmes including: + +- the Sixth Framework Programme for Research and Technological Development + - [DILIGENT](https://cordis.europa.eu/project/id/004260) (grant no. 004260). +- the Seventh Framework Programme for research, technological development and demonstration + - [D4Science](https://cordis.europa.eu/project/id/212488) (grant no. 212488); + - [D4Science-II](https://cordis.europa.eu/project/id/239019) (grant no.239019); + - [ENVRI](https://cordis.europa.eu/project/id/283465) (grant no. 283465); + - [iMarine](https://cordis.europa.eu/project/id/283644) (grant no. 283644); + - [EUBrazilOpenBio](https://cordis.europa.eu/project/id/288754) (grant no. 288754). +- the H2020 research and innovation programme + - [SoBigData](https://cordis.europa.eu/project/id/654024) (grant no. 654024); + - [PARTHENOS](https://cordis.europa.eu/project/id/654119) (grant no. 654119); + - [EGI-Engage](https://cordis.europa.eu/project/id/654142) (grant no. 654142); + - [ENVRI PLUS](https://cordis.europa.eu/project/id/654182) (grant no. 654182); + - [BlueBRIDGE](https://cordis.europa.eu/project/id/675680) (grant no. 675680); + - [PerformFISH](https://cordis.europa.eu/project/id/727610) (grant no. 727610); + - [AGINFRA PLUS](https://cordis.europa.eu/project/id/731001) (grant no. 731001); + - [DESIRA](https://cordis.europa.eu/project/id/818194) (grant no. 818194); + - [ARIADNEplus](https://cordis.europa.eu/project/id/823914) (grant no. 823914); + - [RISIS 2](https://cordis.europa.eu/project/id/824091) (grant no. 824091); + - [EOSC-Pillar](https://cordis.europa.eu/project/id/857650) (grant no. 857650); + - [Blue Cloud](https://cordis.europa.eu/project/id/862409) (grant no. 862409); + - [SoBigData-PlusPlus](https://cordis.europa.eu/project/id/871042) (grant no. 871042); diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3af0507 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,312 @@ +# European Union Public Licence V. 1.1 + + +EUPL © the European Community 2007 + + +This European Union Public Licence (the “EUPL”) applies to the Work or Software +(as defined below) which is provided under the terms of this Licence. Any use of +the Work, other than as authorised under this Licence is prohibited (to the +extent such use is covered by a right of the copyright holder of the Work). + +The Original Work is provided under the terms of this Licence when the Licensor +(as defined below) has placed the following notice immediately following the +copyright notice for the Original Work: + +Licensed under the EUPL V.1.1 + +or has expressed by any other mean his willingness to license under the EUPL. + + + +## 1. Definitions + +In this Licence, the following terms have the following meaning: + +- The Licence: this Licence. + +- The Original Work or the Software: the software distributed and/or + communicated by the Licensor under this Licence, available as Source Code and + also as Executable Code as the case may be. + +- Derivative Works: the works or software that could be created by the Licensee, + based upon the Original Work or modifications thereof. This Licence does not + define the extent of modification or dependence on the Original Work required + in order to classify a work as a Derivative Work; this extent is determined by + copyright law applicable in the country mentioned in Article 15. + +- The Work: the Original Work and/or its Derivative Works. + +- The Source Code: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- The Executable Code: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- The Licensor: the natural or legal person that distributes and/or communicates + the Work under the Licence. + +- Contributor(s): any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- The Licensee or “You”: any natural or legal person who makes any usage of the + Software under the terms of the Licence. + +- Distribution and/or Communication: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, on-line or off-line, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + + + +## 2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a world-wide, royalty-free, non-exclusive, +sub-licensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, reproduce the Work, modify +- the Original Work, and make Derivative Works based upon the Work, communicate +- to the public, including the right to make available or display the Work or +- copies thereof to the public and perform publicly, as the case may be, the +- Work, distribute the Work or copies thereof, lend and rent the Work or copies +- thereof, sub-license rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + + + +## 3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute and/or communicate the Work. + + + +## 4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Original Work or Software, of the exhaustion of those rights or of other +applicable limitations thereto. + + + +## 5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: the Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes and/or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes and/or communicates copies of the +Original Works or Derivative Works based upon the Original Work, this +Distribution and/or Communication will be done under the terms of this Licence +or of a later version of this Licence unless the Original Work is expressly +distributed only under this version of the Licence. The Licensee (becoming +Licensor) cannot offer or impose any additional terms or conditions on the Work +or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes and/or Communicates Derivative +Works or copies thereof based upon both the Original Work and another work +licensed under a Compatible Licence, this Distribution and/or Communication can +be done under the terms of this Compatible Licence. For the sake of this clause, +“Compatible Licence” refers to the licences listed in the appendix attached to +this Licence. Should the Licensee’s obligations under the Compatible Licence +conflict with his/her obligations under this Licence, the obligations of the +Compatible Licence shall prevail. + +Provision of Source Code: When distributing and/or communicating copies of the +Work, the Licensee will provide a machine-readable copy of the Source Code or +indicate a repository where this Source will be easily and freely available for +as long as the Licensee continues to distribute and/or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + + + +## 6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + + + +## 7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +contributors. It is not a finished work and may therefore contain defects or +“bugs” inherent to this type of software development. + +For the above reason, the Work is provided under the Licence on an “as is” basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + + + +## 8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such +damage. However, the Licensor will be liable under statutory product liability +laws as far such laws apply to the Work. + + + +## 9. Additional agreements + +While distributing the Original Work or Derivative Works, You may choose to +conclude an additional agreement to offer, and charge a fee for, acceptance of +support, warranty, indemnity, or other liability obligations and/or services +consistent with this Licence. However, in accepting such obligations, You may +act only on your own behalf and on your sole responsibility, not on behalf of +the original Licensor or any other Contributor, and only if You agree to +indemnify, defend, and hold each Contributor harmless for any liability incurred +by, or claims asserted against such Contributor by the fact You have accepted +any such warranty or additional liability. + + + +## 10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon “I agree” +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution and/or Communication by You of the Work or copies thereof. + + + +## 11. Information to the public + +In case of any Distribution and/or Communication of the Work by means of +electronic communication by You (for example, by offering to download the Work +from a remote location) the distribution channel or media (for example, a +website) must at least provide to the public the information requested by the +applicable law regarding the Licensor, the Licence and the way it may be +accessible, concluded, stored and reproduced by the Licensee. + + + +## 12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + + + +## 13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work licensed hereunder. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed and/or reformed so as necessary to make +it valid and enforceable. + +The European Commission may publish other linguistic versions and/or new +versions of this Licence, so far this is required and reasonable, without +reducing the scope of the rights granted by the Licence. New versions of the +Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + + + +## 14. Jurisdiction + +Any litigation resulting from the interpretation of this License, arising +between the European Commission, as a Licensor, and any Licensee, will be +subject to the jurisdiction of the Court of Justice of the European Communities, +as laid down in article 238 of the Treaty establishing the European Community. + +Any litigation arising between Parties, other than the European Commission, and +resulting from the interpretation of this License, will be subject to the +exclusive jurisdiction of the competent court where the Licensor resides or +conducts its primary business. + + + +## 15. Applicable Law + +This Licence shall be governed by the law of the European Union country where +the Licensor resides or has his registered office. + +This licence shall be governed by the Belgian law if: + +- a litigation arises between the European Commission, as a Licensor, and any +- Licensee; the Licensor, other than the European Commission, has no residence +- or registered office inside a European Union country. + + + +## Appendix + + + +“Compatible Licences” according to article 5 EUPL are: + + +- GNU General Public License (GNU GPL) v. 2 + +- Open Software License (OSL) v. 2.1, v. 3.0 + +- Common Public License v. 1.0 + +- Eclipse Public License v. 1.0 + +- Cecill v. 2.0 + diff --git a/README.md b/README.md index 23d81a8..607d775 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ -# catalogue-core +# Catalogue Core Library + +This service allows any client to publish on the gCube Catalogue. + +## Built With + +* [OpenJDK](https://openjdk.java.net/) - The JDK used +* [Maven](https://maven.apache.org/) - Dependency Management + +## Documentation + +[Catalogue Core Library](https://wiki.gcube-system.org/gcube/GCat_Service) + +## Change log + +See [CHANGELOG.md](CHANGELOG.md). + +## Authors + +* **Luca Frosini** ([ORCID](https://orcid.org/0000-0003-3183-2291)) - [ISTI-CNR Infrascience Group](http://nemis.isti.cnr.it/groups/infrascience) + +## How to Cite this Software + +Tell people how to cite this software. +* Cite an associated paper? +* Use a specific BibTeX entry for the software? + + @software{gcat, + author = {{Luca Frosini}}, + title = {Catalogue Core Library}, + abstract = {Catalogue Core Library allows the publication of items in the gCube Catalogue.}, + url = {}, + keywords = {Catalogue, D4Science, gCube} + } + +## License + +This project is licensed under the EUPL V.1.1 License - see the [LICENSE.md](LICENSE.md) file for details. + + +## About the gCube Framework +This software is part of the [gCubeFramework](https://www.gcube-system.org/ "gCubeFramework"): an +open-source software toolkit used for building and operating Hybrid Data +Infrastructures enabling the dynamic deployment of Virtual Research Environments +by favouring the realisation of reuse oriented policies. + +The projects leading to this software have received funding from a series of European Union programmes see [FUNDING.md](FUNDING.md) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6672048 --- /dev/null +++ b/pom.xml @@ -0,0 +1,221 @@ + + 4.0.0 + + org.gcube.tools + maven-parent + 1.1.0 + + org.gcube.data-catalogue + catalogue-core + 1.0.0-SNAPSHOT + Catalogue Core Library + + + UTF-8 + ${project.basedir}${file.separator}src${file.separator}main${file.separator}webapp${file.separator}WEB-INF + 2.14.0 + + + + scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git + scm:git:https://code-repo.d4science.org/gCubeSystem/${project.artifactId}.git + https://code-repo.d4science.org/gCubeSystem/${project.artifactId} + + + + + + org.gcube.distribution + gcube-smartgears-bom + 2.5.0 + pom + import + + + + + + + + org.gcube.data-catalogue + gcat-api + [2.3.2, 3.0.0-SNAPSHOT) + + + org.gcube.core + common-smartgears + + + org.gcube.core + common-smartgears-app + + + + + org.slf4j + slf4j-api + + + org.gcube.core + common-encryption + + + org.gcube.core + common-scope + + + org.gcube.social-networking + social-service-client + [1.0.0, 2.0.0-SNAPSHOT) + + + org.gcube.common + authorization-utils + [2.2.0, 3.0.0-SNAPSHOT) + + + org.gcube.common + storagehub-model + + + org.gcube.information-system + information-system-model + + + org.gcube.resource-management + gcube-model + + + org.gcube.information-system + resource-registry-client + + + org.gcube.information-system + resource-registry-publisher + + + org.gcube.information-system + resource-registry-query-template-client + + + org.gcube.resources + common-gcore-resources + + + org.gcube.resources + registry-publisher + + + org.gcube.data-catalogue + gcubedatacatalogue-metadata-discovery + [3.0.0, 4.0.0-SNAPSHOT) + + + log4j + log4j + + + org.slf4j + slf4j-log4j12 + + + + + org.gcube.portlets.user + uri-resolver-manager + [1.0.0, 2.0.0-SNAPSHOT) + + + + org.gcube.common + gxHTTP + + + org.gcube.common + keycloak-client + + + + org.postgresql + postgresql + 42.2.19 + + + + + + javax.cache + cache-api + 1.0.0 + + + org.ehcache + ehcache + 3.5.2 + runtime + + + + + + de.grundid.opendatalab + geojson-jackson + 1.8 + + + + org.gcube.data-publishing + storagehub-application-persistence + [3.3.0-SNAPSHOT,4.0.0-SNAPSHOT) + + + + + org.apache.tika + tika-core + 2.1.0 + + + + + commons-lang + commons-lang + 2.6 + + + + + + org.gcube.common + gcube-jackson-core + + + org.gcube.common + gcube-jackson-annotations + + + org.gcube.common + gcube-jackson-databind + + + + + junit + junit + 4.11 + test + + + ch.qos.logback + logback-classic + test + + + \ No newline at end of file diff --git a/src/main/java/org/gcube/gcat/configuration/CatalogueConfigurationFactory.java b/src/main/java/org/gcube/gcat/configuration/CatalogueConfigurationFactory.java new file mode 100644 index 0000000..a04997d --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/CatalogueConfigurationFactory.java @@ -0,0 +1,150 @@ +package org.gcube.gcat.configuration; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxy; +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxyFactory; +import org.gcube.gcat.configuration.isproxies.impl.FacetBasedISConfigurationProxyFactory; +import org.gcube.gcat.configuration.isproxies.impl.GCoreISConfigurationProxyFactory; +import org.gcube.gcat.configuration.service.ServiceCatalogueConfiguration; +import org.gcube.gcat.persistence.ckan.cache.CKANUserCache; +import org.gcube.gcat.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CatalogueConfigurationFactory { + + private static final Logger logger = LoggerFactory.getLogger(CatalogueConfigurationFactory.class); + + private static final Map catalogueConfigurations; + + private static List> factories; + + static { + catalogueConfigurations = new HashMap<>(); + factories = new ArrayList<>(); + } + + private static List> getFactories(){ + if(factories.size()==0) { + factories.add(new FacetBasedISConfigurationProxyFactory()); + factories.add(new GCoreISConfigurationProxyFactory()); + } + return factories; + } + + public static void addISConfigurationProxyFactory(ISConfigurationProxyFactory icpf) { + factories.add(icpf); + } + + private static ServiceCatalogueConfiguration load(String context) { + ServiceCatalogueConfiguration serviceCatalogueConfiguration = null; + SecretManager secretManager = SecretManagerProvider.instance.get(); + try { + Secret secret = Constants.getCatalogueSecret(); + secretManager.startSession(secret); + + for(ISConfigurationProxyFactory icpf : getFactories()) { + try { + ISConfigurationProxy icp = icpf.getInstance(context); + + serviceCatalogueConfiguration = icp.getCatalogueConfiguration(); + logger.trace("The configuration has been read using {}.", icp.getClass().getSimpleName()); + }catch(Exception e){ + logger.warn("{} cannot be used to read {}. Reason is {}", icpf.getClass().getSimpleName(), ServiceCatalogueConfiguration.class.getSimpleName(), e.getMessage()); + } + } + + } catch(Exception e) { + logger.error("Unable to start session. Reason is " + e.getMessage()); + } finally { + secretManager.endSession(); + } + + if(serviceCatalogueConfiguration==null) { + throw new RuntimeException("Unable to load " + ServiceCatalogueConfiguration.class.getSimpleName() + " by using configured " + ISConfigurationProxyFactory.class.getSimpleName() + " i.e. " + getFactories()); + } + return serviceCatalogueConfiguration; + } + + private static void purgeFromIS(String context) { + SecretManager secretManager = SecretManagerProvider.instance.get(); + try { + Secret secret = Constants.getCatalogueSecret(); + secretManager.startSession(secret); + for(ISConfigurationProxyFactory icpf : getFactories()) { + ISConfigurationProxy icp = icpf.getInstance(context); + icp.delete(); + } + } catch(Exception e) { + logger.error("Unable to start session. Reason is " + e.getMessage()); + } finally { + secretManager.endSession(); + } + } + + private static void createOrUpdateOnIS(String context, ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + SecretManager secretManager = SecretManagerProvider.instance.get(); + try { + Secret secret = Constants.getCatalogueSecret(); + secretManager.startSession(secret); + + for(ISConfigurationProxyFactory icpf : getFactories()) { + ISConfigurationProxy icp = icpf.getInstance(context); + icp.setCatalogueConfiguration(catalogueConfiguration); + icp.createOrUpdateOnIS(); + } + + } finally { + secretManager.endSession(); + } + } + + public synchronized static ServiceCatalogueConfiguration getInstance() { + String context = SecretManagerProvider.instance.get().getContext(); + ServiceCatalogueConfiguration catalogueConfiguration = catalogueConfigurations.get(context); + if(catalogueConfiguration == null) { + catalogueConfiguration = load(context); + catalogueConfigurations.put(context, catalogueConfiguration); + } + return catalogueConfiguration; + } + + public synchronized static void renew() { + String context = SecretManagerProvider.instance.get().getContext(); + catalogueConfigurations.remove(context); + ServiceCatalogueConfiguration catalogueConfiguration = load(context); + catalogueConfigurations.put(context, catalogueConfiguration); + } + + public synchronized static void purge() { + // Remove the resource from IS + String context = SecretManagerProvider.instance.get().getContext(); + catalogueConfigurations.remove(context); + purgeFromIS(context); + } + + public synchronized static ServiceCatalogueConfiguration createOrUpdate(ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + String context = SecretManagerProvider.instance.get().getContext(); + catalogueConfigurations.remove(context); + + createOrUpdateOnIS(context, catalogueConfiguration); + catalogueConfigurations.put(context, catalogueConfiguration); + + // The supported organizations could be changed we need to empty the user cache for the context + // to avoid to miss to add an user in an organization which has been added. + CKANUserCache.emptyUserCache(); + + return catalogueConfiguration; + } + +} diff --git a/src/main/java/org/gcube/gcat/configuration/Version.java b/src/main/java/org/gcube/gcat/configuration/Version.java new file mode 100644 index 0000000..40fae1c --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/Version.java @@ -0,0 +1,127 @@ +package org.gcube.gcat.configuration; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class Version implements Comparable { + + /** + * Regex validating the version + */ + public final static String VERSION_REGEX = "^[1-9][0-9]{0,}\\.(0|([1-9][0-9]{0,}))\\.(0|([1-9][0-9]{0,}))"; + + private final static Pattern VERSION_PATTERN; + + static { + VERSION_PATTERN = Pattern.compile(VERSION_REGEX); + } + + protected int major; + protected int minor; + protected int revision; + + protected Version(){} + + public Version(String version) { + setVersion(version); + } + + public Version(int major, int minor, int revision) { + this.major = major; + this.minor = minor; + this.revision = revision; + } + + public void setVersion(String version) { + Matcher matcher = VERSION_PATTERN.matcher(version); + if(!matcher.find()) { + throw new RuntimeException("The provided version (i.e. " + version + ") MUST comply with the regex " + VERSION_REGEX); + } + + String matched = matcher.group(0); + String[] parts = matched.split("\\."); + this.major = Integer.valueOf(parts[0]); + this.minor = Integer.valueOf(parts[1]); + this.revision = Integer.valueOf(parts[2]); + } + + public int getMajor() { + return major; + } + + protected void setMajor(int major) { + this.major = major; + } + + public int getMinor() { + return minor; + } + + protected void setMinor(int minor) { + this.minor = minor; + } + + public int getRevision() { + return revision; + } + + protected void setRevision(int revision) { + this.revision = revision; + } + + @Override + public String toString() { + return major + "." + minor + "." + revision; + } + @Override + public int compareTo(Version other) { + if(other == null) { + return 1; + } + + int compare = Integer.compare(major, other.major); + if(compare!=0) { + return compare; + } + + compare = Integer.compare(minor, other.minor); + if(compare!=0) { + return compare; + } + + compare = Integer.compare(revision, other.revision); + return compare; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + major; + result = prime * result + minor; + result = prime * result + revision; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Version other = (Version) obj; + if (major != other.major) + return false; + if (minor != other.minor) + return false; + if (revision != other.revision) + return false; + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxy.java b/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxy.java new file mode 100644 index 0000000..11cda35 --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxy.java @@ -0,0 +1,72 @@ +package org.gcube.gcat.configuration.isproxies; + +import javax.ws.rs.WebApplicationException; + +import org.gcube.gcat.configuration.Version; +import org.gcube.gcat.configuration.service.ServiceCatalogueConfiguration; +import org.gcube.smartgears.ContextProvider; +import org.gcube.smartgears.configuration.application.ApplicationConfiguration; +import org.gcube.smartgears.context.application.ApplicationContext; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class ISConfigurationProxy { + + protected final String context; + protected ServiceCatalogueConfiguration catalogueConfiguration; + + public ISConfigurationProxy(String context) { + this.context = context; + } + + public ISConfigurationProxy(String context, ServiceCatalogueConfiguration catalogueConfiguration) { + this(context); + this.catalogueConfiguration = catalogueConfiguration; + } + + public ServiceCatalogueConfiguration getCatalogueConfiguration() throws WebApplicationException { + if (catalogueConfiguration == null) { + catalogueConfiguration = readFromIS(); + } + return catalogueConfiguration; + } + + public void setCatalogueConfiguration(ServiceCatalogueConfiguration catalogueConfiguration) { + this.catalogueConfiguration = catalogueConfiguration; + } + + public ServiceCatalogueConfiguration createOrUpdateOnIS() throws Exception { + ISResource isResource = getISResource(); + if(isResource!=null) { + // It's an update + catalogueConfiguration = updateOnIS(); + }else { + // It's a create + catalogueConfiguration = createOnIS(); + } + return catalogueConfiguration; + } + + protected Version getGcatVersion() { + try { + ApplicationContext applicationContext = ContextProvider.get(); + ApplicationConfiguration applicationConfiguration = applicationContext.configuration(); + Version version = new Version(applicationConfiguration.version()); + return version; + }catch (Exception e) { + return new Version("2.4.2"); + } + } + + protected abstract ServiceCatalogueConfiguration createOnIS() throws Exception; + + protected abstract ISResource getISResource(); + + protected abstract ServiceCatalogueConfiguration readFromIS(); + + protected abstract ServiceCatalogueConfiguration updateOnIS() throws Exception; + + public abstract void delete(); + +} diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxyFactory.java b/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxyFactory.java new file mode 100644 index 0000000..f58007c --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/ISConfigurationProxyFactory.java @@ -0,0 +1,36 @@ +package org.gcube.gcat.configuration.isproxies; + +import java.util.HashMap; +import java.util.Map; + +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class ISConfigurationProxyFactory> { + + protected final Map isConfigurationProxies; + + public ISConfigurationProxyFactory() { + this.isConfigurationProxies = new HashMap<>(); + } + + protected abstract ISCP newInstance(String context); + + public synchronized ISCP getInstance(String context) { + ISCP isConfigurationProxy = isConfigurationProxies.get(context); + if(isConfigurationProxy == null) { + isConfigurationProxy = newInstance(context); + isConfigurationProxies.put(context, isConfigurationProxy); + } + return isConfigurationProxy; + } + + public ISCP getInstance() { + String context = SecretManagerProvider.instance.get().getContext(); + return getInstance(context); + } + +} + diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxy.java b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxy.java new file mode 100644 index 0000000..a14d652 --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxy.java @@ -0,0 +1,520 @@ +package org.gcube.gcat.configuration.isproxies.impl; + +import java.io.File; +import java.io.FileReader; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.ws.rs.InternalServerErrorException; + +import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxy; +import org.gcube.gcat.configuration.service.FacetBasedISServiceCatalogueConfiguration; +import org.gcube.gcat.configuration.service.ServiceCKANDB; +import org.gcube.gcat.configuration.service.ServiceCatalogueConfiguration; +import org.gcube.informationsystem.model.impl.properties.PropagationConstraintImpl; +import org.gcube.informationsystem.model.impl.relations.ConsistsOfImpl; +import org.gcube.informationsystem.model.reference.entities.Entity; +import org.gcube.informationsystem.model.reference.properties.Encrypted; +import org.gcube.informationsystem.model.reference.properties.PropagationConstraint; +import org.gcube.informationsystem.model.reference.properties.PropagationConstraint.AddConstraint; +import org.gcube.informationsystem.model.reference.properties.PropagationConstraint.DeleteConstraint; +import org.gcube.informationsystem.model.reference.properties.PropagationConstraint.RemoveConstraint; +import org.gcube.informationsystem.model.reference.relations.ConsistsOf; +import org.gcube.informationsystem.queries.templates.reference.entities.QueryTemplate; +import org.gcube.informationsystem.resourceregistry.api.exceptions.NotFoundException; +import org.gcube.informationsystem.resourceregistry.api.exceptions.ResourceRegistryException; +import org.gcube.informationsystem.resourceregistry.api.exceptions.types.SchemaViolationException; +import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClient; +import org.gcube.informationsystem.resourceregistry.client.ResourceRegistryClientFactory; +import org.gcube.informationsystem.resourceregistry.publisher.ResourceRegistryPublisher; +import org.gcube.informationsystem.resourceregistry.publisher.ResourceRegistryPublisherFactory; +import org.gcube.informationsystem.resourceregistry.queries.templates.ResourceRegistryQueryTemplateClient; +import org.gcube.informationsystem.resourceregistry.queries.templates.ResourceRegistryQueryTemplateClientFactory; +import org.gcube.informationsystem.serialization.ElementMapper; +import org.gcube.resourcemanagement.model.impl.entities.facets.SimpleFacetImpl; +import org.gcube.resourcemanagement.model.impl.entities.resources.EServiceImpl; +import org.gcube.resourcemanagement.model.impl.relations.isrelatedto.CallsForImpl; +import org.gcube.resourcemanagement.model.reference.entities.facets.AccessPointFacet; +import org.gcube.resourcemanagement.model.reference.entities.facets.SimpleFacet; +import org.gcube.resourcemanagement.model.reference.entities.resources.Configuration; +import org.gcube.resourcemanagement.model.reference.entities.resources.EService; +import org.gcube.resourcemanagement.model.reference.entities.resources.VirtualService; +import org.gcube.resourcemanagement.model.reference.relations.isrelatedto.CallsFor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class FacetBasedISConfigurationProxy extends ISConfigurationProxy { + + private static Logger logger = LoggerFactory.getLogger(FacetBasedISConfigurationProxy.class); + + public final String QUERY_TEMPLATE_DIRECTORY_NAME = "query-template"; + public final String SERVICE_ESERVICE_UUID_VARNAME = "$uuid"; + public final String GET_CALLS_FOR_QUERY_TEMPLATE_FILENAME = "01-get-calls-for-query-template.json"; + + public final String QUERY_DIRECTORY_NAME = "query"; + public final String GET_CATALOGUE_VIRTUAL_SERVICE_FILENAME = "01-get-catalogue-virtual-service.json"; + public final String GET_CATALOGUE_CONFIGURATION_FILENAME = "02-get-catalogue-configuration.json"; + public final String GET_SIMPLE_FACET_OF_CATALOGUE_CONFIGURATION_FILENAME = "03-get-simple-facet-of-catalogue-configuration.json"; + public final String GET_ACCESS_POINT_FACET_OF_CKAN_SERVICE_FILENAME = "05-get-access-point-facet-of-ckan-service.json"; + public final String GET_ACCESS_POINT_FACET_OF_POSTGRES_CKAN_DB_FILENAME = "07-get-access-point-facet-of-postgres-ckan-db.json"; + public final String GET_ACCESS_POINT_FACET_OF_SOLR_SERVICE_FILENAME = "09-get-access-point-facet-of-solr-service.json"; + + protected QueryTemplate queryTemplate; + protected String serviceName; + + public QueryTemplate getQueryTemplateFromFile(String queryTemplateFilename) throws Exception { + File queryTemplateFile = getJsonQueryTemplateFromFile(queryTemplateFilename); + FileReader fileReader = new FileReader(queryTemplateFile); + QueryTemplate queryTemplate = ElementMapper.unmarshal(QueryTemplate.class, fileReader); + return queryTemplate; + } + + public QueryTemplate installQueryTemplate() throws Exception { + /* + * Going to create/update the query template. + * No need to test if exists and/or if is the last version. + */ + queryTemplate = rrqtc.update(queryTemplate); + return queryTemplate; + } + + protected File getBaseDirectory(String directoryName) { + URL directoryURL = this.getClass().getClassLoader().getResource(directoryName); + File directory = new File(directoryURL.getPath()); + return directory; + } + + protected File getFile(String directoryName, String filename) throws Exception { + File directory = getBaseDirectory(directoryName); + return new File(directory, filename); + } + + protected File getJsonQueryTemplateFromFile(String filename) throws Exception { + return getFile(QUERY_TEMPLATE_DIRECTORY_NAME, filename); + } + + protected File getJsonQueryFromFile(String filename) throws Exception { + return getFile(QUERY_DIRECTORY_NAME, filename); + } + +// +// Configuration +// --------------------------- +// IsCustomizedBy | | +// -----------------> | catalogue-configuration | +// / | | +// / --------------------------- +// EService VirtualService / +// ------------ ----------------------------- +// | | CallsFor | | +// | gcat | ------------> | catalogue-virtual-service | +// | | | | +// ------------ ----------------------------- +// \ EService +// \ -------------------- +// \ Uses | | +// \ ------------------> | postgres-ckan-db | +// \ / | | +// \ EService / -------------------- +// \ ----------------- +// \ CallsFor | | +// -------------> | ckan | +// | | +// ----------------- EService +// \ -------------------- +// \ Uses | | +// ------------------> | solr | +// | | +// -------------------- + + /* + * Some resources are not needed to be queried and maintained. + * Leaving comment to remember that is not an error + * protected Configuration configuration; + * + * public static final String GET_CKAN_SERVICE_FILENAME = "04-get-ckan-service.json"; + * protected EService ckanService; + * + * public static final String GET_POSTGRES_CKAN_DB_FILENAME = "06-get-postgres-ckan-db.json"; + * protected EService solrService; + * + * public static final String GET_SOLR_SERVICE_FILENAME = "08-get-solr-service.json"; + * protected EService ckanDB; + * + */ + + protected final ObjectMapper objectMapper; + protected final ResourceRegistryClient resourceRegistryClient; + protected final ResourceRegistryPublisher resourceRegistryPublisher; + protected final ResourceRegistryQueryTemplateClient rrqtc; + + /* + * We need to keep this resource because we want to create + * an IsRelatedTo relation + * i.e. EService(gcat) --CallsFor--> VirtualService(catalogue-virtual-service) + */ + protected VirtualService virtualService; + + protected SimpleFacet configurationSimpleFacet; + + protected String serviceEServiceID; + + public FacetBasedISConfigurationProxy(String context) { + super(context); + + serviceName = "gcat"; + + resourceRegistryClient = ResourceRegistryClientFactory.create(); + resourceRegistryPublisher = ResourceRegistryPublisherFactory.create(); + rrqtc = ResourceRegistryQueryTemplateClientFactory.create(); + + try { + queryTemplate = getQueryTemplateFromFile(GET_CALLS_FOR_QUERY_TEMPLATE_FILENAME); + }catch(Exception e) { + throw new RuntimeException(this.getClass().getSimpleName() + " cannot be used", e); + } + objectMapper = new ObjectMapper(); + } + + public VirtualService getVirtualService() { + if(virtualService==null) { + virtualService = queryVirtualService(); + } + return virtualService; + } + + public void setServiceEServiceID(String serviceEServiceID) { + this.serviceEServiceID = serviceEServiceID; + } + + public List> getCallsForToVirtualService() throws Exception { + ResourceRegistryQueryTemplateClient rrqtc = ResourceRegistryQueryTemplateClientFactory.create(); + + ObjectNode objectNode = objectMapper.createObjectNode(); + objectNode.put(SERVICE_ESERVICE_UUID_VARNAME, serviceEServiceID); + + List> callsForList = rrqtc.run(queryTemplate.getName(), objectNode); + return callsForList; + } + + public List> deleteCallsForToVirtualService(List> callsForList) throws SchemaViolationException, NotFoundException, ResourceRegistryException { + for(CallsFor cf : callsForList) { + resourceRegistryPublisher.delete(cf); + } + return callsForList; + } + + public List> deleteCallsForToVirtualService() throws Exception { + List> callsForList = getCallsForToVirtualService(); + return deleteCallsForToVirtualService(callsForList); + } + + public CallsFor createCallsForToVirtualService() throws Exception { + List> callsForList = getCallsForToVirtualService(); + + CallsFor callsFor = null; + + int size = callsForList.size(); + + UUID serviceEServiceUUID = UUID.fromString(serviceEServiceID); + + if(size>1) { + logger.warn("There are {} instances of {} relation beetween {} Eservice with UUID {} and the {} (catalogue-virtual-service). This is very strange because there should be only one. We are going to delete them and recreated a new one.", + callsForList.size(), CallsFor.NAME, serviceName, serviceEServiceID, VirtualService.NAME); + logger.trace("{} relation instances that are going to be deleted are {}", + CallsFor.NAME, ElementMapper.marshal(callsForList)); + deleteCallsForToVirtualService(callsForList); + size = 0; + } + + if(size==0) { + logger.info("Going to create {} between {} ({} with UUID={}) and the {}", + CallsFor.NAME, EService.NAME, serviceName, serviceEServiceID, VirtualService.NAME); + PropagationConstraint propagationConstraint = new PropagationConstraintImpl(); + propagationConstraint.setAddConstraint(AddConstraint.unpropagate); + propagationConstraint.setRemoveConstraint(RemoveConstraint.keep); + propagationConstraint.setDeleteConstraint(DeleteConstraint.keep); + EService serviceEService = new EServiceImpl(); + serviceEService.setID(serviceEServiceUUID); + VirtualService virtualService = queryVirtualService(); + callsFor = new CallsForImpl(serviceEService, virtualService, propagationConstraint); + callsFor = resourceRegistryPublisher.create(callsFor); + }else if(size==1){ + callsFor = callsForList.get(0); + logger.info("{} between {} ({} with UUID={}) and the {} exists.\n{}", + CallsFor.NAME, EService.NAME, serviceName, serviceEServiceID, VirtualService.NAME, ElementMapper.marshal(callsFor)); + } + + return callsFor; + } + + protected List queryListOfEntities(String query) throws Exception { + logger.trace("Going to request the following query:\n{}", query); + String result = resourceRegistryClient.jsonQuery(query); + logger.trace("The query:\n{}\nproduced the following result:\n{}", query, result); + List entities = ElementMapper.unmarshalList(Entity.class, result); + return entities; + } + + protected JsonNode getQuery(File jsonQueryFile) throws Exception { + JsonNode query = objectMapper.readTree(jsonQueryFile); + return query; + } + + protected E getUniqueEntity(List entities, String originalQuery) throws JsonProcessingException { + int size = entities.size(); + if(entities==null || size==0) { + String message = String.format("No instance found with query:\n%s", originalQuery); + logger.error(message); + throw new InternalServerErrorException(message); + } + + if(size>1) { + String message = String.format( + "Too many instances found (i.e. expected 1, found %d) with query:\n%s\nthe obtained result is:\n%s", + size, originalQuery, ElementMapper.marshal(entities)); + logger.error(message); + throw new InternalServerErrorException(message); + } + + return entities.get(0); + } + + protected Entity queryEntity(String filename) throws Exception{ + File jsonQueryFile = getJsonQueryFromFile(filename); + JsonNode query = getQuery(jsonQueryFile); + String jsonQueryAsString = objectMapper.writeValueAsString(query); + List entities = queryListOfEntities(jsonQueryAsString); + return getUniqueEntity(entities, jsonQueryAsString); + } + + protected VirtualService queryVirtualService() throws InternalServerErrorException { + try { + VirtualService virtualService = (VirtualService) queryEntity(GET_CATALOGUE_VIRTUAL_SERVICE_FILENAME); + return virtualService; + }catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected Configuration queryServiceConfiguration() throws Exception { + Configuration configuration = (Configuration) queryEntity(GET_CATALOGUE_CONFIGURATION_FILENAME); + return configuration; + } + + protected SimpleFacet queryConfigurationSimpleFacet() throws Exception { + SimpleFacet configurationSimpleFacet = (SimpleFacet) queryEntity(GET_SIMPLE_FACET_OF_CATALOGUE_CONFIGURATION_FILENAME); + return configurationSimpleFacet; + } + + protected AccessPointFacet queryCkanServiceAccessPointFacet() throws Exception { + AccessPointFacet accessPointFacet = (AccessPointFacet) queryEntity(GET_ACCESS_POINT_FACET_OF_CKAN_SERVICE_FILENAME); + return accessPointFacet; + } + + protected AccessPointFacet queryPostgresCkanDBAccessPointFacet() throws Exception { + AccessPointFacet accessPointFacet = (AccessPointFacet) queryEntity(GET_ACCESS_POINT_FACET_OF_POSTGRES_CKAN_DB_FILENAME); + return accessPointFacet; + } + + public AccessPointFacet querySolrServiceAccessPointFacet() throws Exception { + AccessPointFacet accessPointFacet = (AccessPointFacet) queryEntity(GET_ACCESS_POINT_FACET_OF_SOLR_SERVICE_FILENAME); + return accessPointFacet; + } + + protected ServiceCatalogueConfiguration setConfigurationInfoFromSimpleFacet(SimpleFacet configurationSimpleFacet, ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + if(configurationSimpleFacet.getID()!=null) { + catalogueConfiguration.setID(configurationSimpleFacet.getID().toString()); + } + + catalogueConfiguration.setModerationEnabled((boolean) configurationSimpleFacet.getAdditionalProperty(CatalogueConfiguration.MODERATION_ENABLED_KEY)); + catalogueConfiguration.setNotificationToUsersEnabled((boolean) configurationSimpleFacet.getAdditionalProperty(CatalogueConfiguration.NOTIFICATION_TO_USER_ENABLED_KEY)); + catalogueConfiguration.setSocialPostEnabled((boolean) configurationSimpleFacet.getAdditionalProperty(CatalogueConfiguration.SOCIAL_POST_ENABLED_KEY)); + + catalogueConfiguration.setDefaultOrganization((String) configurationSimpleFacet.getAdditionalProperty(CatalogueConfiguration.DEFAULT_ORGANIZATION_KEY)); + + Object supportedOrganizationsObj = configurationSimpleFacet.getAdditionalProperty(CatalogueConfiguration.SUPPORTED_ORGANIZATIONS_KEY); + + boolean forceUpdate = false; + + if(supportedOrganizationsObj!=null && supportedOrganizationsObj instanceof Collection) { + @SuppressWarnings("unchecked") + Set supportedOrganizations = new HashSet((Collection) supportedOrganizationsObj); + catalogueConfiguration.setSupportedOrganizations(supportedOrganizations); + }else { + Set supportedOrganizations = new HashSet<>(); + supportedOrganizations.add(catalogueConfiguration.getDefaultOrganization()); + configurationSimpleFacet.setAdditionalProperty(CatalogueConfiguration.SUPPORTED_ORGANIZATIONS_KEY, supportedOrganizations); + forceUpdate = true; + catalogueConfiguration.setSupportedOrganizations(supportedOrganizations); + } + + Map additionalProperties = new HashMap<>(configurationSimpleFacet.getAdditionalProperties()); + + for(String key : additionalProperties.keySet()) { + if(!CatalogueConfiguration.KNOWN_PROPERTIES.contains(key)) { + if(key.startsWith("@")) { + continue; + } + Object value = additionalProperties.get(key); + catalogueConfiguration.setAdditionalProperty(key, value); + } + } + + if(forceUpdate) { + updateOnIS(); + } + + return catalogueConfiguration; + } + + public ServiceCatalogueConfiguration setCkanServiceInfo(ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + AccessPointFacet ckanServiceAccessPointFacet = queryCkanServiceAccessPointFacet(); + catalogueConfiguration.setCkanURL(ckanServiceAccessPointFacet.getEndpoint().toString()); + Encrypted encrypted = (Encrypted) ckanServiceAccessPointFacet.getAdditionalProperty(CatalogueConfiguration.SYS_ADMIN_TOKEN_KEY); + String encryptedPassword = encrypted.getValue(); + catalogueConfiguration.setSysAdminToken(encryptedPassword); + return catalogueConfiguration; + } + + public ServiceCatalogueConfiguration setCkanDBInfo(ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + AccessPointFacet postgresCkanDBAccessPointFacet = queryPostgresCkanDBAccessPointFacet(); + ServiceCKANDB ckanDB = new ServiceCKANDB(); + String ckanDbURL = postgresCkanDBAccessPointFacet.getEndpoint().toString(); + ckanDB.setUrl(ckanDbURL); + Encrypted encrypted = (Encrypted) postgresCkanDBAccessPointFacet.getAdditionalProperty(ServiceCKANDB.PASSWORD_KEY); + String encryptedPassword = encrypted.getValue(); + ckanDB.setEncryptedPassword(encryptedPassword); + String username = (String) postgresCkanDBAccessPointFacet.getAdditionalProperty(ServiceCKANDB.USERNAME_KEY); + ckanDB.setUsername(username); + catalogueConfiguration.setCkanDB(ckanDB); + return catalogueConfiguration; + } + + public ServiceCatalogueConfiguration setSolrServiceInfo(ServiceCatalogueConfiguration catalogueConfiguration) throws Exception { + AccessPointFacet solrServiceAccessPointFacet = querySolrServiceAccessPointFacet(); + String solrURL = solrServiceAccessPointFacet.getEndpoint().toString(); + catalogueConfiguration.setSolrURL(solrURL); + return catalogueConfiguration; + } + + protected SimpleFacet getSimpleFacetFromConfiguration(ServiceCatalogueConfiguration catalogueConfiguration) { + SimpleFacet simpleFacet = new SimpleFacetImpl(); + if(catalogueConfiguration.getID()!=null) { + UUID uuid = null; + try { + uuid = UUID.fromString(catalogueConfiguration.getID()); + simpleFacet.setID(uuid); + }catch (Exception e) { + + } + } + + simpleFacet.setAdditionalProperty(CatalogueConfiguration.MODERATION_ENABLED_KEY, catalogueConfiguration.isModerationEnabled()); + simpleFacet.setAdditionalProperty(CatalogueConfiguration.NOTIFICATION_TO_USER_ENABLED_KEY, catalogueConfiguration.isNotificationToUsersEnabled()); + simpleFacet.setAdditionalProperty(CatalogueConfiguration.SOCIAL_POST_ENABLED_KEY, catalogueConfiguration.isSocialPostEnabled()); + + simpleFacet.setAdditionalProperty(CatalogueConfiguration.DEFAULT_ORGANIZATION_KEY, catalogueConfiguration.getDefaultOrganization()); + simpleFacet.setAdditionalProperty(CatalogueConfiguration.SUPPORTED_ORGANIZATIONS_KEY, catalogueConfiguration.getSupportedOrganizations()); + + Map additionalProperties = new HashMap<>(catalogueConfiguration.getAdditionalProperties()); + + for(String key : additionalProperties.keySet()) { + if(!CatalogueConfiguration.KNOWN_PROPERTIES.contains(key)) { + Object value = additionalProperties.get(key); + simpleFacet.setAdditionalProperty(key, value); + } + } + + return simpleFacet; + } + + @Override + protected ServiceCatalogueConfiguration readFromIS() { + try { + catalogueConfiguration = new FacetBasedISServiceCatalogueConfiguration(context, this); + + configurationSimpleFacet = getISResource(); + + if(configurationSimpleFacet==null) { + configurationSimpleFacet = getSimpleFacetFromConfiguration(catalogueConfiguration); + }else { + catalogueConfiguration = setConfigurationInfoFromSimpleFacet(configurationSimpleFacet, catalogueConfiguration); + } + + catalogueConfiguration = setCkanServiceInfo(catalogueConfiguration); + catalogueConfiguration = setCkanDBInfo(catalogueConfiguration); + catalogueConfiguration = setSolrServiceInfo(catalogueConfiguration); + + }catch (InternalServerErrorException e) { + throw e; + }catch (Exception e) { + throw new InternalServerErrorException(e); + } + + return catalogueConfiguration; + } + + @Override + public void delete() { + SimpleFacet simpleFacet = getISResource(); + if(simpleFacet!=null) { + try { + resourceRegistryPublisher.delete(simpleFacet); + } catch (Exception e) { + throw new InternalServerErrorException("Unable to delete SimpleFacet with UUID " + simpleFacet.getID().toString(), e); + } + } + } + + @Override + protected ServiceCatalogueConfiguration createOnIS() throws Exception { + UUID uuid = configurationSimpleFacet.getID(); + if(uuid==null) { + Configuration configuration = queryServiceConfiguration(); + ConsistsOf co = new ConsistsOfImpl<>(configuration, configurationSimpleFacet); + co = resourceRegistryPublisher.create(co); + configurationSimpleFacet = co.getTarget(); + setConfigurationInfoFromSimpleFacet(configurationSimpleFacet, catalogueConfiguration); + } + return catalogueConfiguration; + } + + @Override + protected SimpleFacet getISResource() { + if(configurationSimpleFacet==null) { + try { + configurationSimpleFacet = queryConfigurationSimpleFacet(); + }catch (Exception e) { + return null; + } + } + return configurationSimpleFacet; + } + + @Override + protected ServiceCatalogueConfiguration updateOnIS() throws Exception { + if(configurationSimpleFacet.getID()!=null) { + configurationSimpleFacet = getSimpleFacetFromConfiguration(catalogueConfiguration); + configurationSimpleFacet = resourceRegistryPublisher.update(configurationSimpleFacet); + setConfigurationInfoFromSimpleFacet(configurationSimpleFacet, catalogueConfiguration); + } + return catalogueConfiguration; + } + + +} diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxyFactory.java b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxyFactory.java new file mode 100644 index 0000000..2c941ef --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/FacetBasedISConfigurationProxyFactory.java @@ -0,0 +1,20 @@ +package org.gcube.gcat.configuration.isproxies.impl; + +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxyFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class FacetBasedISConfigurationProxyFactory extends ISConfigurationProxyFactory { + + public FacetBasedISConfigurationProxyFactory() { + super(); + } + + @Override + protected FacetBasedISConfigurationProxy newInstance(String context) { + return new FacetBasedISConfigurationProxy(context); + } + + +} diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxy.java b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxy.java new file mode 100644 index 0000000..e70585a --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxy.java @@ -0,0 +1,676 @@ +package org.gcube.gcat.configuration.isproxies.impl; + +import static org.gcube.resources.discovery.icclient.ICFactory.clientFor; +import static org.gcube.resources.discovery.icclient.ICFactory.queryFor; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; + +import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.resources.gcore.GenericResource; +import org.gcube.common.resources.gcore.ServiceEndpoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.AccessPoint; +import org.gcube.common.resources.gcore.ServiceEndpoint.Profile; +import org.gcube.common.resources.gcore.ServiceEndpoint.Property; +import org.gcube.common.resources.gcore.ServiceEndpoint.Runtime; +import org.gcube.common.resources.gcore.common.Platform; +import org.gcube.common.resources.gcore.utils.Group; +import org.gcube.gcat.api.GCatConstants; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.configuration.Version; +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxy; +import org.gcube.gcat.configuration.service.ServiceCKANDB; +import org.gcube.gcat.configuration.service.ServiceCatalogueConfiguration; +import org.gcube.informationsystem.publisher.RegistryPublisher; +import org.gcube.informationsystem.publisher.RegistryPublisherFactory; +import org.gcube.resources.discovery.client.api.DiscoveryClient; +import org.gcube.resources.discovery.client.queries.api.SimpleQuery; +import org.gcube.resources.discovery.icclient.ICFactory; +import org.gcube.smartgears.ContextProvider; +import org.gcube.smartgears.configuration.container.ContainerConfiguration; +import org.gcube.smartgears.context.application.ApplicationContext; +import org.gcube.smartgears.context.container.ContainerContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GCoreISConfigurationProxy extends ISConfigurationProxy { + + private static final Logger logger = LoggerFactory.getLogger(GCoreISConfigurationProxy.class); + + // property to retrieve the master service endpoint into the /root scope + public final static String IS_ROOT_MASTER_PROPERTY_KEY = "IS_ROOT_MASTER"; // true, false.. missing means false as + + public final static String DEFAULT_ORGANIZATION_PROPERTY_KEY = "DEFAULT_ORGANIZATION"; + public final static String SUPPORTED_ORGANIZATION_PROPERTY_KEY = "SUPPORTED_ORGANIZATION"; + + public final static String API_KEY_PROPERTY_KEY = "API_KEY"; + public final static String SOLR_INDEX_ADDRESS_PROPERTY_KEY = "SOLR_INDEX_ADDRESS"; + public final static String SOCIAL_POST_PROPERTY_KEY = "SOCIAL_POST"; + public final static String ALERT_USERS_ON_POST_CREATION_PROPERTY_KEY = "ALERT_USERS_ON_POST_CREATION"; + public final static String MODERATION_ENABLED_KEY_PROPERTY_KEY = "MODERATION_ENABLED"; + + public static final Map gCoreToConfigurationMapping; + public static final Map configurationToGCoreMapping; + + static { + gCoreToConfigurationMapping = new HashMap<>(); + configurationToGCoreMapping = new HashMap<>(); + + gCoreToConfigurationMapping.put(API_KEY_PROPERTY_KEY, CatalogueConfiguration.SYS_ADMIN_TOKEN_KEY); + + gCoreToConfigurationMapping.put(SOLR_INDEX_ADDRESS_PROPERTY_KEY, CatalogueConfiguration.SOLR_URL_KEY); + + gCoreToConfigurationMapping.put(SOCIAL_POST_PROPERTY_KEY, CatalogueConfiguration.SOCIAL_POST_ENABLED_KEY); + gCoreToConfigurationMapping.put(ALERT_USERS_ON_POST_CREATION_PROPERTY_KEY, CatalogueConfiguration.NOTIFICATION_TO_USER_ENABLED_KEY); + gCoreToConfigurationMapping.put(MODERATION_ENABLED_KEY_PROPERTY_KEY, CatalogueConfiguration.MODERATION_ENABLED_KEY); + + for(String key : gCoreToConfigurationMapping.keySet()) { + configurationToGCoreMapping.put(gCoreToConfigurationMapping.get(key), key); + } + } + + // CKAN Instance info + private final static String OLD_CATEGORY = "Application"; + private final static String OLD_NAME = "CKanDataCatalogue"; + + protected ObjectMapper mapper; + protected ServiceEndpoint serviceEndpoint; + + public GCoreISConfigurationProxy(String context) { + super(context); + this.mapper = new ObjectMapper(); + } + + protected AccessPoint getAccessPoint(Profile profile) { + Group accessPoints = profile.accessPoints(); + Iterator accessPointIterator = accessPoints.iterator(); + AccessPoint accessPoint = accessPointIterator.next(); + return accessPoint; + } + + protected String getDefaultSolrURL(String ckanURL) { + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append(ckanURL); + stringBuffer.append(ckanURL.endsWith("/")?"":"/"); + stringBuffer.append("solr/"); + return stringBuffer.toString(); + } + + protected ObjectNode setValue(ObjectNode node, String key, String value) throws IOException { + if(value.toLowerCase().compareTo("true")==0 || value.toLowerCase().compareTo("false")==0) { + node.put(key, Boolean.parseBoolean(value)); + return node; + } + + if(value.startsWith("{") || value.startsWith("[")){ + JsonNode n = mapper.readTree(value); + node.set(key, n); + return node; + + } + + node.put(key, value); + return node; + } + + protected ServiceCatalogueConfiguration getConfiguration(ServiceEndpoint serviceEndpoint) throws IOException { + Profile profile = serviceEndpoint.profile(); + AccessPoint accessPoint = getAccessPoint(profile); + Map propertyMap = accessPoint.propertyMap(); + + ObjectNode node = mapper.createObjectNode(); + node.put(CatalogueConfiguration.ID_KEY, serviceEndpoint.id()); + + for(String key : propertyMap.keySet()) { + String value = propertyMap.get(key).value().trim(); + setValue(node, key, value); + } + + return mapper.treeToValue(node, ServiceCatalogueConfiguration.class); + } + + private List getServiceEndpoints(String category, String name) { + SimpleQuery query = queryFor(ServiceEndpoint.class); + query.addCondition("$resource/Scopes/Scope/text() eq '" + SecretManagerProvider.instance.get().getContext() + "'"); + query.addCondition("$resource/Profile/Category/text() eq '" + category + "'"); + query.addCondition("$resource/Profile/Name/text() eq '" + name + "'"); + DiscoveryClient client = clientFor(ServiceEndpoint.class); + List serviceEndpoints = client.submit(query); + return serviceEndpoints; + } + + @Override + protected ServiceEndpoint getISResource() { + if(serviceEndpoint==null) { + List serviceEndpoints = getServiceEndpoints(GCatConstants.CONFIGURATION_CATEGORY, GCatConstants.CONFIGURATION_NAME); + if (serviceEndpoints==null || serviceEndpoints.size() == 0) { + logger.error("There is no {} having Category {} and Name {} in this context.", + ServiceEndpoint.class.getSimpleName(), GCatConstants.CONFIGURATION_CATEGORY, GCatConstants.CONFIGURATION_NAME); + return null; + } + serviceEndpoint = serviceEndpoints.get(0); + } + return serviceEndpoint; + } + + @Override + protected ServiceCatalogueConfiguration readFromIS() throws WebApplicationException { + ServiceEndpoint serviceEndpoint = getISResource(); + if(serviceEndpoint==null) { + return getOLDCatalogueConfigurationFromGCoreIS(); + } + try { + return getConfiguration(serviceEndpoint); + }catch (Exception e) { + throw new InternalServerErrorException(); + } + } + + @Deprecated + private ServiceEndpoint getOldServiceEndpoint() { + List serviceEndpoints = getServiceEndpoints(OLD_CATEGORY, OLD_NAME); + + if (serviceEndpoints.size() == 0) { + logger.error("There is no {} having Category {} and Name {} in this context.", + ServiceEndpoint.class.getSimpleName(), OLD_CATEGORY, OLD_NAME); + return null; + } + + ServiceEndpoint serviceEndpoint = null; + + if (serviceEndpoints.size() > 1) { + logger.info("Too many {} having Category {} and Name {} in this context. Looking for the one that has the property {}", + ServiceEndpoint.class.getSimpleName(), OLD_CATEGORY, OLD_NAME, IS_ROOT_MASTER_PROPERTY_KEY); + + for (ServiceEndpoint se : serviceEndpoints) { + Iterator accessPointIterator = se.profile().accessPoints().iterator(); + while (accessPointIterator.hasNext()) { + ServiceEndpoint.AccessPoint accessPoint = accessPointIterator.next(); + + // get the is master property + Property entry = accessPoint.propertyMap().get(IS_ROOT_MASTER_PROPERTY_KEY); + String isMaster = entry != null ? entry.value() : null; + + if (isMaster == null || !isMaster.equals("true")) { + continue; + } + + // set this variable + serviceEndpoint = se; + return serviceEndpoint; + } + } + + // if none of them was master, throw an exception + if (serviceEndpoint == null) { + throw new InternalServerErrorException( + "Too many catalogue configuration on IS and no one with MASTER property"); + } + + } else { + serviceEndpoint = serviceEndpoints.get(0); + } + + return serviceEndpoint; + } + + @Deprecated + protected ServiceCatalogueConfiguration getOLDCatalogueConfigurationFromGCoreIS() { + ServiceCatalogueConfiguration catalogueConfiguration = new ServiceCatalogueConfiguration(context); + try { + // boolean mustBeUpdated = false; + + ServiceEndpoint serviceEndpoint = getOldServiceEndpoint(); + if (serviceEndpoint == null) { + throw new NotFoundException("No configuration found in this context"); + } + + // catalogueConfiguration.setID(serviceEndpoint.id()); + + Profile profile = serviceEndpoint.profile(); + AccessPoint accessPoint = getAccessPoint(profile); + + // add this host + String ckanURL = accessPoint.address(); + catalogueConfiguration.setCkanURL(ckanURL); + + Map propertyMap = accessPoint.propertyMap(); + + // retrieve sys admin token + String encryptedSysAdminToken = propertyMap.get(API_KEY_PROPERTY_KEY).value(); + catalogueConfiguration.setEncryptedSysAdminToken(encryptedSysAdminToken); + + + String defaultOrganization = CatalogueConfiguration.getOrganizationName(context); + + + String solrURL = null; + if (propertyMap.containsKey(SOLR_INDEX_ADDRESS_PROPERTY_KEY)) { + solrURL = propertyMap.get(SOLR_INDEX_ADDRESS_PROPERTY_KEY).value(); + }else { + solrURL = getDefaultSolrURL(ckanURL); + } + catalogueConfiguration.setSolrURL(solrURL); + + // retrieve option to check if the social post has to be made + Boolean socialPostEnabled = true; + if (propertyMap.containsKey(SOCIAL_POST_PROPERTY_KEY)) { + if (propertyMap.get(SOCIAL_POST_PROPERTY_KEY).value().trim().equalsIgnoreCase("false")) { + socialPostEnabled = false; + } + } + catalogueConfiguration.setSocialPostEnabled(socialPostEnabled); + + // retrieve option for user alert + boolean notificationToUsersEnabled = false; // default is false + if (propertyMap.containsKey(ALERT_USERS_ON_POST_CREATION_PROPERTY_KEY)) { + if (propertyMap.get(ALERT_USERS_ON_POST_CREATION_PROPERTY_KEY).value().trim() + .equalsIgnoreCase("true")) { + notificationToUsersEnabled = true; + } + } + catalogueConfiguration.setNotificationToUsersEnabled(notificationToUsersEnabled); + + boolean moderationEnabled = false; // default is false + if (propertyMap.containsKey(MODERATION_ENABLED_KEY_PROPERTY_KEY)) { + if (propertyMap.get(MODERATION_ENABLED_KEY_PROPERTY_KEY).value().trim().equalsIgnoreCase("true")) { + moderationEnabled = true; + } + } + catalogueConfiguration.setModerationEnabled(moderationEnabled); + + Set supportedOrganizations = getSupportedOrganizationsFromGenericResource(); + if (supportedOrganizations != null) { + catalogueConfiguration.setSupportedOrganizations(supportedOrganizations); + if(defaultOrganization==null) { + defaultOrganization = supportedOrganizations.toArray(new String[supportedOrganizations.size()])[0]; + catalogueConfiguration.setDefaultOrganization(defaultOrganization); + } + } + + ServiceCKANDB ckanDB = getCKANDBFromIS(); + catalogueConfiguration.setCkanDB(ckanDB); + + } catch (WebApplicationException e) { + throw e; + } catch (Exception e) { + throw new InternalServerErrorException("Error while getting configuration on IS", e); + } + + return catalogueConfiguration; + } + + // CKAN Instance info + @Deprecated + private final static String CKAN_DB_SERVICE_ENDPOINT_CATEGORY= "Database"; + @Deprecated + private final static String CKAN_DB_SERVICE_ENDPOINT_NAME = "CKanDatabase"; + + @Deprecated + protected ServiceCKANDB getCKANDBFromIS() { + try { + List serviceEndpoints = getServiceEndpoints(CKAN_DB_SERVICE_ENDPOINT_CATEGORY, CKAN_DB_SERVICE_ENDPOINT_NAME); + if(serviceEndpoints.size() == 0) { + String error = String.format("There is no %s having category '%s' and name '%s' in this context.", + ServiceEndpoint.class.getSimpleName(), CKAN_DB_SERVICE_ENDPOINT_CATEGORY, CKAN_DB_SERVICE_ENDPOINT_NAME); + logger.error(error); + throw new InternalServerErrorException(error); + } + + ServiceEndpoint serviceEndpoint = null; + + if(serviceEndpoints.size() > 1) { + logger.info("Too many {} having category {} and name {} in this context. Looking for the one that has the property {}", + ServiceEndpoint.class.getSimpleName(), CKAN_DB_SERVICE_ENDPOINT_CATEGORY, + CKAN_DB_SERVICE_ENDPOINT_NAME); + + for(ServiceEndpoint se : serviceEndpoints) { + Iterator accessPointIterator = se.profile().accessPoints().iterator(); + while(accessPointIterator.hasNext()) { + ServiceEndpoint.AccessPoint accessPoint = accessPointIterator.next(); + + // get the is master property + Property entry = accessPoint.propertyMap().get(IS_ROOT_MASTER_PROPERTY_KEY); + String isMaster = entry != null ? entry.value() : null; + + if(isMaster == null || !isMaster.equals("true")) { + continue; + } + + // set this variable + serviceEndpoint = se; + break; + } + } + + // if none of them was master, throw an exception + if(serviceEndpoint == null) { + throw new InternalServerErrorException( + "Too many CKAN configuration on IS and no one with MASTER property"); + } + + } else { + serviceEndpoint = serviceEndpoints.get(0); + } + + Iterator accessPointIterator = serviceEndpoint.profile().accessPoints().iterator(); + while(accessPointIterator.hasNext()) { + AccessPoint accessPoint = accessPointIterator.next(); + + String host = accessPoint.address(); + String db = accessPoint.name(); + + ServiceCKANDB ckanDB = new ServiceCKANDB(); + String url = String.format("jdbc:postgresql://%s/%s", host, db); + ckanDB.setUrl(url); + ckanDB.setUsername(accessPoint.username()); + ckanDB.setEncryptedPassword(accessPoint.password()); + return ckanDB; + } + + return null; + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException("Error while getting configuration on IS", e); + } + + } + + @Deprecated + public static final String GENERIC_RESOURCE_SECONDARY_TYPE_FOR_ORGANIZATIONS = "ApplicationProfile"; + @Deprecated + public static final String GENERIC_RESOURCE_NAME_FOR_ORGANIZATIONS = "Supported CKAN Organizations"; + @Deprecated + public static final String GENERIC_RESOURCE_CKAN_ORGANIZATIONS = "CKANOrganizations"; + + @Deprecated + private List getGenericResources() { + SimpleQuery query = ICFactory.queryFor(GenericResource.class); + query.addCondition(String.format("$resource/Profile/SecondaryType/text() eq '%s'", + GENERIC_RESOURCE_SECONDARY_TYPE_FOR_ORGANIZATIONS)); + query.addCondition( + String.format("$resource/Profile/Name/text() eq '%s'", GENERIC_RESOURCE_NAME_FOR_ORGANIZATIONS)); + + DiscoveryClient client = ICFactory.clientFor(GenericResource.class); + List genericResources = client.submit(query); + return genericResources; + } + + protected String marshallSupportedOrganizations() throws JsonProcessingException { + Set supportedOrganizations = catalogueConfiguration.getSupportedOrganizations(); + return marshallSupportedOrganizations(supportedOrganizations); + } + + protected String marshallSupportedOrganizations(Set supportedOrganizations) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + ArrayNode arrayNode = objectMapper.createArrayNode(); + for(String org : supportedOrganizations) { + arrayNode.add(org); + } + return objectMapper.writeValueAsString(arrayNode); + } + + @Deprecated + protected Set unmarshallSupportedOrganizations(String supportedOrganizationsJsonArray){ + try { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = objectMapper.readTree(supportedOrganizationsJsonArray); + ArrayNode array = (ArrayNode) jsonNode.get(GENERIC_RESOURCE_CKAN_ORGANIZATIONS); + Set supportedOrganizations = new HashSet<>(array.size()); + for (int i = 0; i < array.size(); i++) { + String o = array.get(i).asText(); + supportedOrganizations.add(o); + } + + logger.debug("Supported CKAN Organization for current Context ({}) are {}", context, + supportedOrganizations); + + return supportedOrganizations; + } catch (Exception e) { + return null; + } + } + + @Deprecated + protected Set getSupportedOrganizationsFromGenericResource() { + List genericResources = getGenericResources(); + + if (genericResources == null || genericResources.size() == 0) { + logger.trace( + "{} with SecondaryType {} and Name %s not found. Item will be only be created in {} CKAN organization", + GenericResource.class.getSimpleName(), GENERIC_RESOURCE_SECONDARY_TYPE_FOR_ORGANIZATIONS, + GENERIC_RESOURCE_NAME_FOR_ORGANIZATIONS, ServiceCatalogueConfiguration.getOrganizationName(context)); + return null; + } + + GenericResource genericResource = genericResources.get(0); + String supportedOrganizationsJsonArray = genericResource.profile().body().getTextContent(); + + Set supportedOrganizatins = unmarshallSupportedOrganizations(supportedOrganizationsJsonArray); + + return supportedOrganizatins; + } + + @Deprecated + public void deleteOldConfiguration() { + RegistryPublisher registryPublisher = RegistryPublisherFactory.create(); + deleteOldConfiguration(registryPublisher); + } + + @Deprecated + protected void deleteOldConfiguration(RegistryPublisher registryPublisher) { + ServiceEndpoint serviceEndpoint = getOldServiceEndpoint(); + if(serviceEndpoint!=null) { + registryPublisher.remove(serviceEndpoint); + } + + List genericResources = getGenericResources(); + if(genericResources!=null) { + for(GenericResource genericResource : genericResources) { + registryPublisher.remove(genericResource); + } + } + } + + @Override + public void delete() { + RegistryPublisher registryPublisher = RegistryPublisherFactory.create(); + ServiceEndpoint serviceEndpoint = getISResource(); + if(serviceEndpoint!=null) { + registryPublisher.remove(serviceEndpoint); + } + } + + protected Property addProperty(Group properties, String name, String value) { + return addProperty(properties, name, value, false); + } + + protected Property addProperty(Group properties, String name, String value, boolean encrypted) { + Property property = new Property(); + property.nameAndValue(name, value); + property.encrypted(encrypted); + properties.add(property); + return property; + } + + protected Group setAccessPointProperties(AccessPoint accessPoint, String address, boolean update) throws JsonProcessingException { + accessPoint.description(String.format("Access Point %s by gcat %s", update ? "updated" : "created", getGcatVersion().toString())); + accessPoint.address(address); + accessPoint.name(GCatConstants.CONFIGURATION_NAME); + + Group properties = accessPoint.properties(); + + JsonNode jsonNode = mapper.valueToTree(catalogueConfiguration); + Iterator iterator = jsonNode.fieldNames(); + while (iterator.hasNext()) { + String key = iterator.next(); + + if(key.compareTo(CatalogueConfiguration.ID_KEY)==0) { + continue; + } + + if(key.compareTo(CatalogueConfiguration.SYS_ADMIN_TOKEN_KEY)==0) { + addProperty(properties, key, catalogueConfiguration.getEncryptedSysAdminToken(), true); + continue; + } + + JsonNode valueJsonNode = jsonNode.get(key); + + String value = valueJsonNode.toString(); + if(valueJsonNode.isTextual()) { + value = valueJsonNode.asText(); + } + + addProperty(properties, key, value); + + } + return properties; + } + + /** + * Set the version of gcat so that in future implementation + * we can understand if the configuration must be updated. + * @param platform + * @return the platform + */ + protected Platform setVersion(Platform platform) { + Version version = getGcatVersion(); + platform.version((short) version.getMajor()); + platform.minorVersion((short) version.getMinor()); + platform.revisionVersion((short) version.getRevision()); + platform.buildVersion((short) 0); + return platform; + } + + + protected Platform setPlatformProperty(Platform platform) { + /* + * + * gcat + * + * 2 + * 2 + * 0 + * 0 + * + */ + platform.name(GCatConstants.SERVICE_NAME); + platform = setVersion(platform); + return platform; + } + + private String getRunningOn(ContainerConfiguration containerConfiguration) { + return String.format("%s:%s", containerConfiguration.hostname(), containerConfiguration.port()); + } + + protected Runtime setRuntimeProperties(Runtime runtime) { + try { + ApplicationContext applicationContext = ContextProvider.get(); + ContainerContext containerContext = applicationContext.container(); + ContainerConfiguration containerConfiguration = containerContext.configuration(); + String runningOn = getRunningOn(containerConfiguration); + runtime.hostedOn(runningOn); + runtime.ghnId(containerContext.id()); + runtime.status(applicationContext.configuration().mode().toString()); + }catch (Exception e) { + runtime.hostedOn("localhost"); + runtime.ghnId(""); + runtime.status("READY"); + } + return runtime; + } + + protected Profile setProfileProperties(Profile profile, boolean update) { + /* + * + * Application + * CKanDataCatalogue + * gCat Configuration created/updated by the service via REST + */ + profile.category(GCatConstants.CONFIGURATION_CATEGORY); + profile.name(GCatConstants.CONFIGURATION_NAME); + profile.description(String.format("gCat configuration %s by the service via REST", update ? "updated" : "created")); + return profile; + } + +// @Deprecated +// protected boolean isRootMaster(ServiceEndpoint serviceEndpoint) { +// Profile profile = serviceEndpoint.profile(); +// AccessPoint accessPoint = getAccessPoint(profile); +// Map propertyMap = accessPoint.propertyMap(); +// if (propertyMap.containsKey(IS_ROOT_MASTER_PROPERTY_KEY)) { +// if (propertyMap.get(IS_ROOT_MASTER_PROPERTY_KEY).value().trim().equalsIgnoreCase("true")) { +// return true; +// } +// } +// return false; +// } + + protected ServiceEndpoint createServiceEndpoint(ServiceEndpoint serviceEndpoint) throws Exception { + boolean update = true; + if(serviceEndpoint==null) { + serviceEndpoint = new ServiceEndpoint(); + serviceEndpoint.setId(catalogueConfiguration.getID()); + update = false; + } + + Profile profile = serviceEndpoint.newProfile(); + profile = setProfileProperties(profile, update); + + Platform platform = profile.newPlatform(); + setPlatformProperty(platform); + + Runtime runtime = profile.newRuntime(); + runtime = setRuntimeProperties(runtime); + + Group accessPoints = profile.accessPoints(); + AccessPoint accessPoint = accessPoints.add(); + setAccessPointProperties(accessPoint, runtime.hostedOn(), update); + + return serviceEndpoint; + } + + @Override + protected ServiceCatalogueConfiguration createOnIS() throws Exception { + RegistryPublisher registryPublisher = RegistryPublisherFactory.create(); + String id = catalogueConfiguration.getID(); + if(id==null || id.compareTo("")==0) { + id = UUID.randomUUID().toString(); + catalogueConfiguration.setID(id); + } + ServiceEndpoint serviceEndpoint = createServiceEndpoint(null); + registryPublisher.create(serviceEndpoint); + return catalogueConfiguration; + } + + @Override + protected ServiceCatalogueConfiguration updateOnIS() throws Exception { + RegistryPublisher registryPublisher = RegistryPublisherFactory.create(); + ServiceEndpoint serviceEndpoint = getISResource(); + String id = serviceEndpoint.id(); + catalogueConfiguration.setID(id); + serviceEndpoint = createServiceEndpoint(serviceEndpoint); + registryPublisher.update(serviceEndpoint); + return catalogueConfiguration; + } + +} diff --git a/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxyFactory.java b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxyFactory.java new file mode 100644 index 0000000..fc12dcf --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/isproxies/impl/GCoreISConfigurationProxyFactory.java @@ -0,0 +1,23 @@ +package org.gcube.gcat.configuration.isproxies.impl; + +import org.gcube.gcat.configuration.isproxies.ISConfigurationProxyFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GCoreISConfigurationProxyFactory extends ISConfigurationProxyFactory { + + public GCoreISConfigurationProxyFactory() { + super(); + } + + @Override + protected GCoreISConfigurationProxy newInstance(String context) { + GCoreISConfigurationProxy isConfigurationProxy = new GCoreISConfigurationProxy(context); + return isConfigurationProxy; + } + + + + +} diff --git a/src/main/java/org/gcube/gcat/configuration/service/FacetBasedISServiceCatalogueConfiguration.java b/src/main/java/org/gcube/gcat/configuration/service/FacetBasedISServiceCatalogueConfiguration.java new file mode 100644 index 0000000..329c410 --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/service/FacetBasedISServiceCatalogueConfiguration.java @@ -0,0 +1,86 @@ +package org.gcube.gcat.configuration.service; + +import javax.ws.rs.InternalServerErrorException; + +import org.gcube.com.fasterxml.jackson.annotation.JsonGetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore; +import org.gcube.com.fasterxml.jackson.annotation.JsonProperty; +import org.gcube.gcat.configuration.isproxies.impl.FacetBasedISConfigurationProxy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class FacetBasedISServiceCatalogueConfiguration extends ServiceCatalogueConfiguration { + + private static Logger logger = LoggerFactory.getLogger(FacetBasedISServiceCatalogueConfiguration.class); + + protected final FacetBasedISConfigurationProxy facetBasedISConfigurationProxy; + + public FacetBasedISServiceCatalogueConfiguration(String context, FacetBasedISConfigurationProxy facetBasedISConfigurationProxy) { + super(context); + this.facetBasedISConfigurationProxy = facetBasedISConfigurationProxy; + } + + @JsonProperty(value = CKAN_URL_KEY) + public String getCkanURL() { + if(ckanURL==null) { + try { + facetBasedISConfigurationProxy.setCkanServiceInfo(this); + } catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + return ckanURL; + } + + @JsonIgnore + public String getSysAdminToken() { + if(sysAdminToken==null) { + try { + facetBasedISConfigurationProxy.setCkanServiceInfo(this); + } catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + return sysAdminToken; + } + + @JsonGetter(value=SYS_ADMIN_TOKEN_KEY) + public String getEncryptedSysAdminToken() { + if(encryptedSysAdminToken==null) { + try { + facetBasedISConfigurationProxy.setCkanServiceInfo(this); + } catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + return encryptedSysAdminToken; + } + + @JsonGetter(value = CKAN_DB_KEY) + public ServiceCKANDB getCkanDB() { + if(ckanDB==null) { + try { + facetBasedISConfigurationProxy.setCkanDBInfo(this); + } catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + return (ServiceCKANDB) ckanDB; + } + + + @JsonProperty(value = SOLR_URL_KEY) + public String getSolrURL() { + if(solrURL==null) { + try { + facetBasedISConfigurationProxy.setSolrServiceInfo(this); + } catch (Exception e) { + throw new InternalServerErrorException(e); + } + } + return solrURL; + } +} diff --git a/src/main/java/org/gcube/gcat/configuration/service/ServiceCKANDB.java b/src/main/java/org/gcube/gcat/configuration/service/ServiceCKANDB.java new file mode 100644 index 0000000..98e5acc --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/service/ServiceCKANDB.java @@ -0,0 +1,63 @@ +package org.gcube.gcat.configuration.service; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; + +import org.gcube.com.fasterxml.jackson.annotation.JsonGetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore; +import org.gcube.com.fasterxml.jackson.annotation.JsonSetter; +import org.gcube.common.encryption.encrypter.StringEncrypter; +import org.gcube.gcat.api.configuration.CKANDB; + +/** + * @author Luca Frosini (ISTI-CNR) + */ +public class ServiceCKANDB extends CKANDB { + + public static final String USERNAME_KEY = "username"; + public static final String PASSWORD_KEY = "password"; + + protected String encryptedPassword; + + @JsonIgnore + public String getPassword() { + return password; + } + + @JsonIgnore + public String getPlainPassword() { + return password; + } + + @JsonGetter(value=PASSWORD_KEY) + public String getEncryptedPassword() { + return encryptedPassword; + } + + public void setEncryptedPassword(String encryptedPassword) throws Exception { + this.encryptedPassword = encryptedPassword; + this.password = StringEncrypter.getEncrypter().decrypt(encryptedPassword); + } + + public void setPlainPassword(String plainPassword) throws Exception { + this.password = plainPassword; + this.encryptedPassword = StringEncrypter.getEncrypter().encrypt(plainPassword); + } + + @Override + @JsonSetter(value = PASSWORD_KEY) + public void setPassword(String password) { + try { + try { + this.password = StringEncrypter.getEncrypter().decrypt(password); + this.encryptedPassword = password; + }catch (IllegalBlockSizeException | BadPaddingException e) { + this.password = password; + this.encryptedPassword = StringEncrypter.getEncrypter().encrypt(password); + } + }catch (Exception e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/org/gcube/gcat/configuration/service/ServiceCatalogueConfiguration.java b/src/main/java/org/gcube/gcat/configuration/service/ServiceCatalogueConfiguration.java new file mode 100644 index 0000000..7d06fbc --- /dev/null +++ b/src/main/java/org/gcube/gcat/configuration/service/ServiceCatalogueConfiguration.java @@ -0,0 +1,142 @@ +package org.gcube.gcat.configuration.service; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; + +import org.gcube.com.fasterxml.jackson.annotation.JsonGetter; +import org.gcube.com.fasterxml.jackson.annotation.JsonIgnore; +import org.gcube.com.fasterxml.jackson.annotation.JsonSetter; +import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.encryption.encrypter.StringEncrypter; +import org.gcube.gcat.api.configuration.CKANDB; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.api.roles.Role; +import org.gcube.gcat.persistence.ckan.CKANUser; +import org.gcube.gcat.persistence.ckan.cache.CKANUserCache; + +/** + * @author Luca Frosini (ISTI-CNR) + */ +public class ServiceCatalogueConfiguration extends CatalogueConfiguration { + + protected ObjectMapper mapper; + + public ServiceCatalogueConfiguration() { + super(); + mapper = new ObjectMapper(); + } + + public ServiceCatalogueConfiguration(String context) { + super(context); + mapper = new ObjectMapper(); + } + + @JsonIgnore + protected String encryptedSysAdminToken; + + @JsonIgnore + public String getSysAdminToken() { + return sysAdminToken; + } + + @JsonIgnore + public String getPlainSysAdminToken() { + return getSysAdminToken(); + } + + @JsonGetter(value=SYS_ADMIN_TOKEN_KEY) + public String getEncryptedSysAdminToken() { + return encryptedSysAdminToken; + } + + public void setEncryptedSysAdminToken(String encryptedSysAdminToken) throws Exception { + this.encryptedSysAdminToken = encryptedSysAdminToken; + this.sysAdminToken = StringEncrypter.getEncrypter().decrypt(encryptedSysAdminToken); + } + + public void setPlainSysAdminToken(String plainSysAdminToken) throws Exception { + this.sysAdminToken = plainSysAdminToken; + this.encryptedSysAdminToken = StringEncrypter.getEncrypter().encrypt(plainSysAdminToken); + } + + @Override + @JsonSetter(value = SYS_ADMIN_TOKEN_KEY) + public void setSysAdminToken(String sysAdminToken) { + try { + try { + this.sysAdminToken = StringEncrypter.getEncrypter().decrypt(sysAdminToken); + this.encryptedSysAdminToken = sysAdminToken; + }catch (IllegalBlockSizeException | BadPaddingException e) { + this.sysAdminToken = sysAdminToken; + this.encryptedSysAdminToken = StringEncrypter.getEncrypter().encrypt(sysAdminToken); + } + }catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + @JsonGetter(value = CKAN_DB_KEY) + public ServiceCKANDB getCkanDB() { + return (ServiceCKANDB) ckanDB; + } + + @Override + public void setCkanDB(CKANDB ckanDB) { + this.ckanDB = new ServiceCKANDB(); + this.ckanDB.setUrl(ckanDB.getUrl()); + this.ckanDB.setUsername(ckanDB.getUsername()); + this.ckanDB.setPassword(ckanDB.getPassword()); + } + + @JsonSetter(value=CKAN_DB_KEY) + public void setCkanDB(ServiceCKANDB ckanDB) { + this.ckanDB = ckanDB; + } + + public ObjectNode toObjetcNode() throws JsonProcessingException { + return toObjetcNode(false); + } + + public ObjectNode toObjetcNode(boolean decryptedValues) throws JsonProcessingException { + ObjectNode configuration = mapper.valueToTree(this); + CKANUser ckanUser = CKANUserCache.getCurrrentCKANUser(); + if(ckanUser.getRole().ordinal() < Role.MANAGER.ordinal()) { + configuration.remove(ServiceCatalogueConfiguration.SYS_ADMIN_TOKEN_KEY); + configuration.remove(ServiceCatalogueConfiguration.CKAN_DB_KEY); + }else { + if(decryptedValues) { + configuration.put(ServiceCatalogueConfiguration.SYS_ADMIN_TOKEN_KEY, getPlainSysAdminToken()); + ObjectNode node = (ObjectNode) configuration.get(ServiceCatalogueConfiguration.CKAN_DB_KEY); + node.put(ServiceCKANDB.PASSWORD_KEY, ((ServiceCKANDB) ckanDB).getPlainPassword()); + } + } + return configuration; + } + + public String toJsonString() throws Exception { + return toJsonString(false); + } + + public String toJsonString(boolean decryptedValues) throws Exception { + ObjectNode objectNode = toObjetcNode(decryptedValues); + return mapper.writeValueAsString(objectNode); + } + + public static ServiceCatalogueConfiguration getServiceCatalogueConfiguration(String json) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ServiceCatalogueConfiguration catalogueConfiguration = mapper.readValue(json, ServiceCatalogueConfiguration.class); + return catalogueConfiguration; + } + + public static ServiceCatalogueConfiguration getServiceCatalogueConfiguration(ObjectNode objectNode) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + ServiceCatalogueConfiguration catalogueConfiguration = mapper.treeToValue(objectNode, ServiceCatalogueConfiguration.class); + return catalogueConfiguration; + } + + + +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/FakeModerationThread.java b/src/main/java/org/gcube/gcat/moderation/thread/FakeModerationThread.java new file mode 100644 index 0000000..20198cb --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/FakeModerationThread.java @@ -0,0 +1,34 @@ +package org.gcube.gcat.moderation.thread; + +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.gcat.api.moderation.CMItemStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class FakeModerationThread extends ModerationThread { + + private static final Logger logger = LoggerFactory.getLogger(FakeModerationThread.class); + + @Override + protected void postMessage(String message) throws Exception { + logger.info("gCat is sending a message to the {} for item '{}' (id={}). ItemStatus={}, Message=\"{}\"", + ModerationThread.class.getSimpleName(), itemName, itemID, cmItemStatus, message); + } + + @Override + public void postUserMessage(CMItemStatus cmItemStatus, String userMessage) throws Exception { + logger.info("{} is sending a message to the {} for item '{}' (id={}). ItemStatus={}, Message=\"{}\"", + SecretManagerProvider.instance.get().getUser().getUsername(), + ModerationThread.class.getSimpleName(), itemName, itemID, cmItemStatus, userMessage); + } + + @Override + protected void createModerationThread() throws Exception { + logger.info("Creating {} for item '{}' (id={})", ModerationThread.class.getSimpleName(), itemName, itemID); + } + + +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/ModerationThread.java b/src/main/java/org/gcube/gcat/moderation/thread/ModerationThread.java new file mode 100644 index 0000000..2abb26a --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/ModerationThread.java @@ -0,0 +1,174 @@ +package org.gcube.gcat.moderation.thread; + +import java.util.HashMap; +import java.util.Map; + +//import java.util.HashMap; +//import java.util.Map; + +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +//import org.gcube.common.authorization.utils.manager.SecretManager; +//import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +//import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.api.moderation.CMItemStatus; +import org.gcube.gcat.moderation.thread.social.notifications.SocialNotificationModerationThread; +import org.gcube.gcat.persistence.ckan.CKANUser; +//import org.gcube.portlets.user.uriresolvermanager.UriResolverManager; +//import org.gcube.portlets.user.uriresolvermanager.resolvers.query.CatalogueResolverQueryString.MODERATION_OP; +//import org.gcube.portlets.user.uriresolvermanager.resolvers.query.CatalogueResolverQueryStringBuilder; +import org.gcube.portlets.user.uriresolvermanager.UriResolverManager; +import org.gcube.portlets.user.uriresolvermanager.resolvers.query.CatalogueResolverQueryString.MODERATION_OP; +import org.gcube.portlets.user.uriresolvermanager.resolvers.query.CatalogueResolverQueryStringBuilder; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class ModerationThread { + + protected String itemID; + protected String itemName; + protected String itemTitle; + protected String itemURL; + protected String itemAuthorCkanUsername; + + protected String moderationURL; + + protected boolean create; + protected CMItemStatus cmItemStatus; + protected boolean itemAuthor; + + protected CKANUser ckanUser; + protected ObjectMapper objectMapper; + + public static ModerationThread getDefaultInstance() { + // return new FakeModerationThread(); + // return new SocialMessageModerationThread(); + return new SocialNotificationModerationThread(); + } + + public ModerationThread() { + this.objectMapper = new ObjectMapper(); + this.itemAuthor = false; + this.create = false; + this.cmItemStatus = CMItemStatus.PENDING; + } + + public void setItemCoordinates(String itemID, String itemName, String itemTitle, String itemURL) { + this.itemID = itemID; + this.itemName = itemName; + this.itemTitle = itemTitle; + this.itemURL = itemURL; + } + + public void setItemAuthor(boolean itemAuthor) { + this.itemAuthor = itemAuthor; + } + + public String getItemAuthorCkanUsername() { + return itemAuthorCkanUsername; + } + + public void setItemAuthorCkanUsername(String itemAuthorCkanUsername) { + this.itemAuthorCkanUsername = itemAuthorCkanUsername; + } + + public void setCKANUser(CKANUser ckanUser) { + this.ckanUser = ckanUser; + } + + public String getModerationURL() { + if(moderationURL==null) { + try { + SecretManager secretManager = SecretManagerProvider.instance.get(); + String context = secretManager.getContext(); + UriResolverManager resolver = new UriResolverManager("CTLG"); + Map params = new HashMap(); + params.put("gcube_scope", context); //e.g. /gcube/devsec/devVRE + params.put("entity_context", "organization"); + params.put("entity_name", CatalogueConfiguration.getOrganizationName(context)); //e.g. devvre + + CatalogueResolverQueryStringBuilder builder = new CatalogueResolverQueryStringBuilder(itemName); //item name under moderation + builder.itemStatus(cmItemStatus.name()). //e.g. pending, approved, rejected + moderation(MODERATION_OP.show); + + String queryString = builder.buildQueryParametersToQueryString(); + params.put(CatalogueResolverQueryStringBuilder.QUERY_STRING_PARAMETER, queryString); + + moderationURL = resolver.getLink(params, true); + }catch (Exception e) { + return itemURL; + } + } + return moderationURL; + } + + /** + * The message is sent as gCat + * @param message + * @throws Exception + */ + protected abstract void postMessage(String message) throws Exception; + + /** + * The message is sent as User + * + * @param cmItemStatus + * @param userMessage + * @throws Exception + */ + public abstract void postUserMessage(CMItemStatus cmItemStatus, String userMessage) throws Exception; + + protected abstract void createModerationThread() throws Exception; + + public void postItemCreated() throws Exception { + createModerationThread(); + this.create = true; + this.cmItemStatus = CMItemStatus.PENDING; + String fullName = ckanUser.getNameSurname(); + String message = String.format( + "@**%s** created the item with name '%s' (id='%s'). The item is now in **%s** state and must be moderated.", + fullName, itemName, itemID, cmItemStatus.getFancyValue()); + postMessage(message); + } + + public void postItemUpdated() throws Exception { + this.cmItemStatus = CMItemStatus.PENDING; + String fullName = ckanUser.getNameSurname(); + String message = String.format( + "@**%s** updated the item with name '%s' (id='%s'). The item is now in **%s** state and must be moderated.", + fullName, itemName, itemID, cmItemStatus.getFancyValue()); + postMessage(message); + } + + public void postItemRejected(String userMessage) throws Exception { + this.cmItemStatus = CMItemStatus.REJECTED; + String fullName = ckanUser.getNameSurname(); + String message = String.format( + "@**%s** **%s** the item with name '%s' (id='%s'). The author can delete the item or update it to try to meet moderators requests if any.", + fullName, cmItemStatus.getFancyValue(), itemName, itemID); + postMessage(message); + postUserMessage(cmItemStatus, userMessage); + } + + public void postItemApproved(String userMessage) throws Exception { + this.cmItemStatus = CMItemStatus.APPROVED; + String fullName = ckanUser.getNameSurname(); + String message = String.format( + "@**%s** **%s** the item with name '%s' (id='%s'). The item is now available in the catalogue. The item is available at %s", + fullName, cmItemStatus.getFancyValue(), itemName, itemID, itemURL); + postMessage(message); + postUserMessage(cmItemStatus, userMessage); + } + + public void postItemDeleted() throws Exception { + this.cmItemStatus = null; + String fullName = ckanUser.getNameSurname(); + String message = String.format( + "@**%s** deleted the item with name '%s' (id='%s')", + fullName, itemName, itemID, itemURL); + postMessage(message); + } +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/social/notifications/SocialNotificationModerationThread.java b/src/main/java/org/gcube/gcat/moderation/thread/social/notifications/SocialNotificationModerationThread.java new file mode 100644 index 0000000..a3fdce9 --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/social/notifications/SocialNotificationModerationThread.java @@ -0,0 +1,335 @@ +package org.gcube.gcat.moderation.thread.social.notifications; + +import java.net.URL; +import java.util.HashSet; +import java.util.Set; + +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.gcat.api.moderation.CMItemStatus; +import org.gcube.gcat.api.moderation.Moderated; +import org.gcube.gcat.moderation.thread.ModerationThread; +import org.gcube.gcat.persistence.ckan.CKANUser; +import org.gcube.gcat.social.SocialUsers; +import org.gcube.gcat.utils.Constants; +import org.gcube.social_networking.social_networking_client_library.NotificationClient; +import org.gcube.social_networking.socialnetworking.model.beans.catalogue.CatalogueEvent; +import org.gcube.social_networking.socialnetworking.model.beans.catalogue.CatalogueEventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SocialNotificationModerationThread extends ModerationThread { + + private static final Logger logger = LoggerFactory.getLogger(SocialNotificationModerationThread.class); + + public static final String AUTHOR = "Author"; + + protected CatalogueEventType catalogueEventType; + protected boolean comment; + + protected boolean notificationSentByGCat; + protected boolean notificationToSelfOnly; + + public SocialNotificationModerationThread() { + super(); + this.comment = false; + this.notificationSentByGCat = false; + this.notificationToSelfOnly = false; + } + + /** + * Create the message for an item that is created/updated + */ + protected void notifyItemToBeManaged() throws Exception { + /* + * An example of created message is: + * + * [mister x] created/updated the item "[TITLE]". You are kindly requested to review it and decide either to APPROVE or REJECT it. [Go to catalogue] + * + */ + String fullName = ckanUser.getNameSurname(); + StringBuffer stringBuffer = new StringBuffer(); + if(notificationSentByGCat) { + stringBuffer.append(fullName); + } + stringBuffer.append(create ? " created " : " updated "); + stringBuffer.append("the item "); + stringBuffer = addQuotedTitle(stringBuffer); + stringBuffer.append(". You are kindly requested to review it and decide either to APPROVE or REJECT it. "); + postMessage(stringBuffer.toString()); + + + notificationToSelfOnly = true; + notificationSentByGCat = true; + stringBuffer = new StringBuffer(); + stringBuffer.append("Thank you for submitting your item "); + stringBuffer = addQuotedTitle(stringBuffer); + stringBuffer.append(" to the catalogue. This item has been successfully received and will be managed by the catalogue moderators."); + postMessage(stringBuffer.toString()); + notificationToSelfOnly = false; + notificationSentByGCat = false; + } + + protected void notifyItemDeleted() throws Exception { + String fullName = ckanUser.getNameSurname(); + StringBuffer stringBuffer = new StringBuffer(); + if(notificationSentByGCat) { + stringBuffer.append(fullName); + } + stringBuffer.append(" permanently deleted "); + stringBuffer.append("the item "); + stringBuffer = addQuotedTitle(stringBuffer); + stringBuffer.append("."); + postMessage(stringBuffer.toString()); + } + + @Override + public void postItemCreated() throws Exception { + create = true; + cmItemStatus = CMItemStatus.PENDING; + catalogueEventType = CatalogueEventType.ITEM_SUBMITTED; + notifyItemToBeManaged(); + } + + @Override + public void postItemUpdated() throws Exception { + create = false; + cmItemStatus = CMItemStatus.PENDING; + catalogueEventType = CatalogueEventType.ITEM_UPDATED; + notifyItemToBeManaged(); + } + + protected StringBuffer addUserWithRole(String fullName, String role, StringBuffer stringBuffer, boolean addUserFullName) { + if(addUserFullName) { + stringBuffer.append(fullName); + } + if(role!=null) { + stringBuffer.append(" ["); + stringBuffer.append(role); + stringBuffer.append("] "); + } + return stringBuffer; + } + + protected StringBuffer addUserWithRole(String fullName, String role, StringBuffer stringBuffer) { + return addUserWithRole(fullName, role, stringBuffer, notificationSentByGCat); + } + + public void postItemManaged(String userMessage) throws Exception { + /* + * [mister x] rejected the item "[TITLE]" with this accompanying message "[MESSAGE]". To resubmit it [Go to catalogue] + * + * [mister x] approved the item "[TITLE]" with this accompanying message "[MESSAGE]". [Go to catalogue] + */ + this.create = false; + String fullName = ckanUser.getNameSurname(); + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer = addUserWithRole(fullName, Moderated.CATALOGUE_MODERATOR, stringBuffer); + stringBuffer.append(cmItemStatus.getValue()); + stringBuffer.append(" the item "); + stringBuffer = addQuotedTitle(stringBuffer); + if(userMessage!=null && userMessage.length()>0) { + stringBuffer.append(" with this accompanying message \""); + stringBuffer.append(userMessage); + stringBuffer.append("\""); + } + stringBuffer.append("."); + + if(cmItemStatus == CMItemStatus.REJECTED) { + stringBuffer.append(" To resubmit it "); + } + postMessage(stringBuffer.toString()); + } + + @Override + public void postItemRejected(String userMessage) throws Exception { + this.create = false; + this.cmItemStatus = CMItemStatus.REJECTED; + this.catalogueEventType = CatalogueEventType.ITEM_REJECTED; + postItemManaged(userMessage); + } + + @Override + public void postItemApproved(String userMessage) throws Exception { + this.create = false; + this.cmItemStatus = CMItemStatus.APPROVED; + this.catalogueEventType = CatalogueEventType.ITEM_PUBLISHED; + postItemManaged(userMessage); + } + + @Override + public void postItemDeleted() throws Exception { + this.create = false; + this.cmItemStatus = null; + this.catalogueEventType = CatalogueEventType.ITEM_REMOVED; + notifyItemDeleted(); + } + + protected StringBuffer addQuotedTitle(StringBuffer stringBuffer, String quotingCharacter) { + stringBuffer.append(quotingCharacter); + stringBuffer.append(itemTitle); + stringBuffer.append(quotingCharacter); + return stringBuffer; + } + + protected StringBuffer addQuotedTitle(StringBuffer stringBuffer) { + return addQuotedTitle(stringBuffer, "\""); + } + + protected String getSubject() { + StringBuffer stringBuffer = new StringBuffer(); + String fullName = ckanUser.getNameSurname(); + if(!comment) { + switch (catalogueEventType) { + case ITEM_SUBMITTED: + stringBuffer.append(fullName); + stringBuffer.append(" created the item "); + break; + + case ITEM_UPDATED: + stringBuffer.append(fullName); + stringBuffer.append(" updated the item "); + break; + + case ITEM_REJECTED: + case ITEM_PUBLISHED: + addUserWithRole(fullName, Moderated.CATALOGUE_MODERATOR, stringBuffer, true); + stringBuffer.append(cmItemStatus.getValue()); + stringBuffer.append(" the item "); + break; + + default: + break; + } + }else { + addUserWithRole(fullName, itemAuthor ? SocialNotificationModerationThread.AUTHOR : Moderated.CATALOGUE_MODERATOR, stringBuffer, true); + stringBuffer.append("commented on the item "); + } + stringBuffer = addQuotedTitle(stringBuffer); + return stringBuffer.toString(); + } + + protected CatalogueEvent getCatalogueEvent(String messageString) throws Exception { + CatalogueEvent catalogueEvent = new CatalogueEvent(); + catalogueEvent.setType(catalogueEventType); + catalogueEvent.setNotifyText(messageString); + catalogueEvent.setItemId(getSubject()); + if(cmItemStatus!=null && cmItemStatus == CMItemStatus.APPROVED) { + catalogueEvent.setItemURL(new URL(itemURL)); + }else { + catalogueEvent.setItemURL(new URL(getModerationURL())); + } + + Set users = new HashSet<>(); + + if(!notificationToSelfOnly) { + users.addAll(SocialUsers.getUsernamesByRole(Moderated.CATALOGUE_MODERATOR)); + + if(itemAuthorCkanUsername!=null) { + // Adding item author + users.add(CKANUser.getUsernameFromCKANUsername(itemAuthorCkanUsername)); + } + } + + // Adding current user + users.add(CKANUser.getUsernameFromCKANUsername(ckanUser.getName())); + + catalogueEvent.setIdsToNotify(users.toArray(new String[users.size()])); + catalogueEvent.setIdsAsGroup(false); + + return catalogueEvent; + } + + @Override + protected void postMessage(String messageString) throws Exception { + CatalogueEvent catalogueEvent = getCatalogueEvent(messageString); + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + if(notificationSentByGCat) { + secretManager.startSession(secret); + } + try { + sendNotification(catalogueEvent); + }finally { + if(notificationSentByGCat) { + secretManager.endSession(); + } + } + } + + @Override + public void postUserMessage(CMItemStatus cmItemStatus, String userMessage) throws Exception { + /* + * [mister x] ([Role]) commented on the item "[TITLE]" as follows "[MESSAGE]". [Go to catalogue] + */ + this.create = false; + this.cmItemStatus = cmItemStatus; + this.comment = true; + + switch (cmItemStatus) { + case PENDING: + catalogueEventType = CatalogueEventType.ITEM_UPDATED; + break; + + case APPROVED: + catalogueEventType = CatalogueEventType.ITEM_PUBLISHED; + break; + + case REJECTED: + catalogueEventType = CatalogueEventType.ITEM_REJECTED; + break; + + default: + break; + } + + String fullName = ckanUser.getNameSurname(); + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer = addUserWithRole(fullName, itemAuthor ? SocialNotificationModerationThread.AUTHOR : Moderated.CATALOGUE_MODERATOR, stringBuffer); + stringBuffer.append("commented on the item "); + stringBuffer = addQuotedTitle(stringBuffer); + stringBuffer.append(" as follows \""); + stringBuffer.append(userMessage); + stringBuffer.append("\"."); + CatalogueEvent catalogueEvent = getCatalogueEvent(stringBuffer.toString()); + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + if(notificationSentByGCat) { + secretManager.startSession(secret); + } + try { + sendNotification(catalogueEvent); + }finally { + if(notificationSentByGCat) { + secretManager.endSession(); + } + } + } + + protected void sendNotification(CatalogueEvent catalogueEvent) throws Exception { + Thread thread = new Thread() { + public void run() { + try { + logger.trace("{} is going to send the following notification {}", SecretManagerProvider.instance.get().getUser().getUsername(), catalogueEvent); + NotificationClient nc = new NotificationClient(); + nc.sendCatalogueEvent(catalogueEvent); + } catch(Exception e) { + logger.error("Error while sending notification.", e); + } + } + }; + // thread.run(); + thread.start(); + } + + @Override + protected void createModerationThread() throws Exception { + create = true; + cmItemStatus = CMItemStatus.PENDING; + } + +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipAuth.java b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipAuth.java new file mode 100644 index 0000000..386c8e1 --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipAuth.java @@ -0,0 +1,43 @@ +package org.gcube.gcat.moderation.thread.zulip; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class ZulipAuth { + + public static final String ZULIP_RC_FILENAME = "zuliprc"; + + public static final String EMAIL_KEY = "email"; + public static final String KEY_KEY = "key"; + public static final String SITE_KEY = "site"; + + protected final Properties properties; + + public ZulipAuth(String username) { + properties = new Properties(); + InputStream input = ZulipAuth.class.getClassLoader().getResourceAsStream(username+"_"+ZULIP_RC_FILENAME); + try { + // load the properties file + properties.load(input); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + public String getEmail() { + return properties.getProperty(EMAIL_KEY); + } + + public String getAPIKey() { + return properties.getProperty(KEY_KEY); + } + + public String getSite() { + return properties.getProperty(SITE_KEY); + } + +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipResponse.java b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipResponse.java new file mode 100644 index 0000000..0832230 --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipResponse.java @@ -0,0 +1,57 @@ +package org.gcube.gcat.moderation.thread.zulip; + +import java.io.IOException; + +import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class ZulipResponse { + + public static final String RESULT_KEY = "result"; + public static final String MSG_KEY = "msg"; + + public enum Result { + success, + error + } + + protected ObjectMapper objectMapper; + + protected String responseString; + protected JsonNode response; + + protected Result result; + protected String message; + + public ZulipResponse(String responseString) { + this.responseString = responseString; + this.objectMapper = new ObjectMapper(); + } + + public Result getResponseResult() throws JsonProcessingException, IOException { + if(result==null) { + String resultString = getResponse().get(RESULT_KEY).asText(); + result = Result.valueOf(resultString); + } + return result; + } + + public String getResponseMessage() throws JsonProcessingException, IOException { + if(message==null) { + message = getResponse().get(MSG_KEY).asText(); + } + return message; + } + + public JsonNode getResponse() throws JsonProcessingException, IOException { + if(response == null) { + response = objectMapper.readTree(responseString); + } + return response; + } + +} diff --git a/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipStream.java b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipStream.java new file mode 100644 index 0000000..d9f1dc6 --- /dev/null +++ b/src/main/java/org/gcube/gcat/moderation/thread/zulip/ZulipStream.java @@ -0,0 +1,171 @@ +package org.gcube.gcat.moderation.thread.zulip; + +//import java.util.Set; +// +//import javax.ws.rs.InternalServerErrorException; +// +//import org.gcube.com.fasterxml.jackson.databind.JsonNode; +//import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +//import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +//import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +//import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.gcat.api.moderation.CMItemStatus; +//import org.gcube.gcat.api.moderation.Moderated; +import org.gcube.gcat.moderation.thread.ModerationThread; +//import org.gcube.gcat.moderation.thread.zulip.ZulipResponse.Result; +//import org.gcube.gcat.social.SocialUsers; +//import org.gcube.gcat.utils.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +//import io.taliox.zulip.ZulipRestExecutor; +//import io.taliox.zulip.calls.ZulipRestAPICall; +//import io.taliox.zulip.calls.messages.PostMessage; +//import io.taliox.zulip.calls.streams.GetStreamID; +//import io.taliox.zulip.calls.streams.PostCreateStream; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class ZulipStream extends ModerationThread { + + private static final Logger logger = LoggerFactory.getLogger(ZulipStream.class); + + @Override + protected void postMessage(String message) throws Exception { + logger.info("gCat is sending a message to the {} for item '{}' (id={}). ItemStatus={}, Message=\"{}\"", + ZulipStream.class.getSimpleName(), itemName, itemID, cmItemStatus, message); + } + + @Override + public void postUserMessage(CMItemStatus cmItemStatus, String userMessage) throws Exception { + logger.info("{} is sending a message to the {} for item '{}' (id={}). ItemStatus={}, Message=\"{}\"", + SecretManagerProvider.instance.get().getUser().getUsername(), + ZulipStream.class.getSimpleName(), itemName, itemID, cmItemStatus, userMessage); + } + + @Override + protected void createModerationThread() throws Exception { + logger.info("Creating {} for item '{}' (id={})", ZulipStream.class.getSimpleName(), itemName, itemID); + } + +// public static final String TOPICS_KEY = "topics"; +// public static final String NAME_KEY = "name"; +// public static final String MAX_ID_KEY = "max_id"; +// public static final String INITIAL_TOPIC_NAME = "hello"; +// +// protected ZulipRestExecutor gCatZulipRestExecutor; +// protected ZulipRestExecutor userZulipRestExecutor; +// +// protected String streamName; +// protected String streamDescription; +// +// public ZulipStream() { +// super(); +// } +// +// protected ZulipRestExecutor getZulipRestExecutor() { +// ZulipAuth zulipAuth = new ZulipAuth(SecretManagerProvider.instance.get().getUser().getUsername()); +// return new ZulipRestExecutor(zulipAuth.getEmail(), zulipAuth.getAPIKey(), zulipAuth.getSite()); +// } +// +// public ZulipRestExecutor getGCatZulipRestExecutor() throws Exception { +// if(gCatZulipRestExecutor==null) { +// SecretManager secretManager = SecretManagerProvider.instance.get(); +// Secret secret = Constants.getCatalogueSecret(); +// secretManager.startSession(secret); +// gCatZulipRestExecutor = getZulipRestExecutor(); +// secretManager.endSession(); +// } +// return gCatZulipRestExecutor; +// } +// +// public ZulipRestExecutor getUserZulipRestExecutor() { +// if(userZulipRestExecutor==null) { +// userZulipRestExecutor = getZulipRestExecutor(); +// } +// return userZulipRestExecutor; +// } +// +// protected String getStreamName() { +// if(streamName==null) { +// streamName = String.format("Item '%s' moderation", itemID); +// } +// return streamName; +// } +// +// protected Integer getStreamID() throws Exception { +// GetStreamID getStreamID = new GetStreamID(getStreamName()); +// ZulipResponse zulipResponse = executeZulipCall(gCatZulipRestExecutor, getStreamID); +// JsonNode response = zulipResponse.getResponse(); +// return response.get("stream_id").asInt(); +// } +// +// protected String getStreamDescription() { +// if(streamDescription==null) { +// streamDescription = String.format("This stream is used to discuss about the moderation of the item '%s' with id '%s'", itemName, itemID); +// } +// return streamDescription; +// } +// +// protected ZulipResponse executeZulipCall(ZulipRestExecutor zulipRestExecutor, ZulipRestAPICall call) throws Exception { +// logger.trace("Going to execute {}", call); +// String responseString = zulipRestExecutor.executeCall(call); +// logger.trace("Response from {} is {}", call.getClass().getSimpleName(), responseString); +// ZulipResponse zulipResponse = new ZulipResponse(responseString); +// if(zulipResponse.getResponseResult()==Result.error) { +// throw new InternalServerErrorException(zulipResponse.getResponseMessage()); +// } +// return zulipResponse; +// } +// +// @Override +// protected void createModerationThread() throws Exception { +// ArrayNode streamsArrayNode = objectMapper.createArrayNode(); +// ObjectNode streamobjectNode = objectMapper.createObjectNode(); +// streamobjectNode.put("name", getStreamName()); +// streamobjectNode.put("description", getStreamDescription()); +// streamsArrayNode.add(streamobjectNode); +// +// ArrayNode principalsArrayNode = objectMapper.createArrayNode(); +// // Going to add the item creator +// String itemCreatorEmail = ckanUser.getEMail(); +// principalsArrayNode.add(itemCreatorEmail); +// +// getGCatZulipRestExecutor(); +// +// principalsArrayNode.add(gCatZulipRestExecutor.httpController.getUserName()); +// +// // Going to add the catalogue moderators +// Set moderators = SocialUsers.getUsernamesByRole(Moderated.CATALOGUE_MODERATOR); +// for(String moderator : moderators) { +// principalsArrayNode.add(moderator); +// } +// +// PostCreateStream postCreateStream = new PostCreateStream(streamsArrayNode.toString()); +// postCreateStream.setPrincipals(principalsArrayNode.toString()); +// postCreateStream.setInvite_only(true); +// postCreateStream.setAnnounce(false); +// +// executeZulipCall(gCatZulipRestExecutor, postCreateStream); +// } +// +// protected void postMessageToStream(ZulipRestExecutor zulipRestExecutor, String message) throws Exception { +// PostMessage postMessage = new PostMessage(getStreamName(), cmItemStatus.getFancyValue(), message); +// logger.debug("Going to send the following message: {}", message); +// executeZulipCall(zulipRestExecutor, postMessage); +// } +// +// @Override +// protected void postMessage(String message) throws Exception { +// postMessageToStream(getGCatZulipRestExecutor(), message); +// } +// +// @Override +// public void postUserMessage(CMItemStatus cmItemStatus, String message) throws Exception { +// this.cmItemStatus = cmItemStatus; +// postMessageToStream(getUserZulipRestExecutor(), message); +// } + +} diff --git a/src/main/java/org/gcube/gcat/oldutils/CustomField.java b/src/main/java/org/gcube/gcat/oldutils/CustomField.java new file mode 100644 index 0000000..8206320 --- /dev/null +++ b/src/main/java/org/gcube/gcat/oldutils/CustomField.java @@ -0,0 +1,131 @@ +package org.gcube.gcat.oldutils; + +import org.gcube.com.fasterxml.jackson.databind.JsonNode; + +/** + * A custom field bean. It also stores index of the category and of the metadata field associated. + * These are used to sort them before pushing the content to CKAN. + * If they are missing, indexes are set to Integer.MAX_VALUE. + * @author Costantino Perciante (ISTI - CNR) + * @author Luca Frosini (ISTI - CNR) + */ +public class CustomField implements Comparable { + + private String key; + private String qualifiedKey; + private String value; + + private int indexCategory = Integer.MAX_VALUE; + private int indexMetadataField = Integer.MAX_VALUE; + + private void init(String key, String value, int indexCategory, int indexMetadataField) { + if(key == null || value == null || key.isEmpty()) { + throw new IllegalArgumentException( + "A custom field must have a key and a value! Provided values are " + key + "=" + value); + } + + this.key = key; + this.qualifiedKey = key; + this.value = value; + + this.indexMetadataField = indexMetadataField; + this.indexCategory = indexCategory; + + if(this.indexCategory < 0) { + this.indexCategory = Integer.MAX_VALUE; + } + + if(this.indexMetadataField < 0) { + this.indexMetadataField = Integer.MAX_VALUE; + } + } + + public CustomField(JsonNode object) { + super(); + init(object.get("key").asText(), object.get("value").asText(), -1, -1); + } + + /** + * @param key + * @param value + */ + public CustomField(String key, String value) { + super(); + init(key, value, -1, -1); + } + + /** + * @param key + * @param value + * @param indexMetadataField + * @param indexCategory + */ + public CustomField(String key, String value, int indexCategory, int indexMetadataField) { + super(); + init(key, value, indexCategory, indexMetadataField); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getQualifiedKey() { + return qualifiedKey; + } + + public void setQualifiedKey(String qualifiedKey) { + this.qualifiedKey = qualifiedKey; + } + + public int getIndexCategory() { + return indexCategory; + } + + public void setIndexCategory(int indexCategory) { + this.indexCategory = indexCategory; + if(this.indexCategory < 0) + this.indexCategory = Integer.MAX_VALUE; + } + + public int getIndexMetadataField() { + return indexMetadataField; + } + + public void setIndexMetadataField(int indexMetadataField) { + this.indexMetadataField = indexMetadataField; + if(this.indexMetadataField < 0) { + this.indexMetadataField = Integer.MAX_VALUE; + } + } + + @Override + public String toString() { + return "CustomField [key=" + key + ", qualifiedKey=" + qualifiedKey + ", value=" + value + + ", indexMetadataField=" + indexMetadataField + ", indexCategory=" + indexCategory + "]"; + } + + @Override + public int compareTo(CustomField o) { + if(this.indexCategory == o.indexCategory) { + if(this.indexMetadataField == o.indexMetadataField) { + return 0; + } else { + return this.indexMetadataField > o.indexMetadataField ? 1 : -1; + } + } else { + return this.indexCategory > o.indexCategory ? 1 : -1; + } + } +} diff --git a/src/main/java/org/gcube/gcat/oldutils/Validator.java b/src/main/java/org/gcube/gcat/oldutils/Validator.java new file mode 100644 index 0000000..1edf8a3 --- /dev/null +++ b/src/main/java/org/gcube/gcat/oldutils/Validator.java @@ -0,0 +1,645 @@ +package org.gcube.gcat.oldutils; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +import org.apache.commons.lang.math.NumberUtils; +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.scope.api.ScopeProvider; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.DataType; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataField; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataFormat; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataGrouping; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataTagging; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataVocabulary; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.NamespaceCategory; +import org.gcube.gcat.persistence.ckan.CKANGroup; +import org.gcube.gcat.persistence.ckan.CKANPackage; +import org.gcube.gcat.persistence.ckan.CKANUser; +import org.gcube.gcat.persistence.ckan.CKANUtility; +import org.gcube.gcat.profile.MetadataUtility; +import org.geojson.GeoJsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Validate creation item requests utilities. + * @author Costantino Perciante (ISTI - CNR) + * @author Luca Frosini (ISTI - CNR) + */ +public class Validator { + + private static final Logger logger = LoggerFactory.getLogger(Validator.class); + + private static final SimpleDateFormat DATE_SIMPLE = new SimpleDateFormat("yyyy-MM-dd"); + private static final SimpleDateFormat DATE_HOUR_MINUTES = new SimpleDateFormat("yyyy-MM-dd HH:mm"); + + public static final int MAX_TAG_CHARS = 100; + + public static final String ITEM_URL = "Item URL"; + + protected ObjectMapper mapper; + + public Validator() { + this.mapper = new ObjectMapper(); + } + + public Validator(ObjectMapper mapper) { + this.mapper = mapper; + } + + public ObjectNode validateAgainstProfile(ObjectNode objectNode, MetadataUtility metadataUtility) throws Exception { + + ArrayNode extrasArrayOriginal = (ArrayNode) objectNode.get(CKANPackage.EXTRAS_KEY); + if(extrasArrayOriginal == null || extrasArrayOriginal.size() == 0) { + throw new BadRequestException( + "'extras' field is missing in context where metadata profile(s) are defined!"); + } + + ArrayNode groupsArrayOriginal = (ArrayNode) objectNode.get(CKANPackage.GROUPS_KEY); + if(groupsArrayOriginal == null) { + groupsArrayOriginal = mapper.createArrayNode(); + } + + ArrayNode tagsArrayOriginal = (ArrayNode) objectNode.get(CKANPackage.TAGS_KEY); + if(tagsArrayOriginal == null) { + tagsArrayOriginal = mapper.createArrayNode(); + } + + // get the metadata profile specifying the type + CustomField metadataTypeCF = null; + List customFields = new ArrayList(extrasArrayOriginal.size()); + Iterator iterator = extrasArrayOriginal.iterator(); + while(iterator.hasNext()) { + JsonNode object = iterator.next(); + CustomField cf = new CustomField(object); + if(cf.getKey().equals(CKANPackage.EXTRAS_KEY_VALUE_SYSTEM_TYPE)) { + metadataTypeCF = cf; + } else if(cf.getKey().equals(ITEM_URL)) { + continue; + } else { + customFields.add(cf); + } + } + + if(metadataTypeCF == null) { + throw new BadRequestException("'" + CKANPackage.EXTRAS_KEY_VALUE_SYSTEM_TYPE + + "' extra field is missing in context where metadata profile(s) are defined!"); + } + + String profileName = metadataTypeCF.getValue(); + // fetch the profile by metadata type specified above + + if(metadataUtility == null) { + metadataUtility = new MetadataUtility(); + } + MetadataFormat profile = metadataUtility.getMetadataFormat(profileName); + if(profile == null) { + throw new BadRequestException("'" + CKANPackage.EXTRAS_KEY_VALUE_SYSTEM_TYPE + "' extra field's value ('" + + profileName + + "') specified as custom field doesn't match any of the profiles defined in this context!"); + } else { + ArrayNode extrasArrayUpdated = null; + List metadataFields = profile.getMetadataFields(); + + if(metadataFields == null || metadataFields.isEmpty()) { + extrasArrayUpdated = extrasArrayOriginal; + } else { + extrasArrayUpdated = mapper.createArrayNode(); + + List categories = metadataUtility.getNamespaceCategories(); + logger.debug("Retrieved namespaces are {}", categories); + List categoriesIds = new ArrayList(categories == null ? 0 : categories.size()); + if(categories == null || categories.isEmpty()) { + logger.warn("No category defined in context {}", ScopeProvider.instance.get()); + } else { + for(NamespaceCategory metadataCategory : categories) { + categoriesIds.add(metadataCategory.getId()); // save them later for matching with custom fields + } + } + + // the list of already validated customFields + List validatedCustomFields = new ArrayList(customFields.size()); + + // keep track of mandatory fields and their cardinality + Map fieldsMandatoryLowerBoundMap = new HashMap(metadataFields.size()); + Map fieldsMandatoryUpperBoundMap = new HashMap(metadataFields.size()); + Map numberFieldsMandatorySameKeyMap = new HashMap( + metadataFields.size()); + + // keep track of the groups that must be created AFTER validation but BEFORE item creation + List groupsToCreateAfterValidation = new ArrayList(); + + // now validate fields + int metadataIndex = 0; + + Map metadataFieldMap = new HashMap<>(); + for(MetadataField metadataField : metadataFields) { + + metadataFieldMap.put(metadataField.getFieldName(), metadataField); + + int categoryIdIndex = categoriesIds.indexOf(metadataField.getCategoryRef()); + logger.debug("Found index for category " + metadataField.getCategoryRef() + " " + categoryIdIndex); + List validCFs = validateAgainstMetadataField(metadataIndex, categoryIdIndex, + customFields, tagsArrayOriginal, groupsArrayOriginal, metadataField, categories, + fieldsMandatoryLowerBoundMap, fieldsMandatoryUpperBoundMap, numberFieldsMandatorySameKeyMap, + groupsToCreateAfterValidation); + validatedCustomFields.addAll(validCFs); + metadataIndex++; + + } + + // check mandatory fields + Iterator> iteratorLowerBounds = fieldsMandatoryLowerBoundMap.entrySet() + .iterator(); + while(iteratorLowerBounds.hasNext()) { + Map.Entry entry = (Map.Entry) iteratorLowerBounds + .next(); + int lowerBound = entry.getValue(); + + // int upperBound = fieldsMandatoryUpperBoundMap.get(entry.getKey()); + int upperBound = Integer.MAX_VALUE; + try { + String maxOccurs = metadataFieldMap.get(entry.getKey()).getMaxOccurs(); + if(maxOccurs==null || maxOccurs.compareTo("*")==0) { + upperBound = Integer.MAX_VALUE; + }else { + try { + upperBound = Integer.valueOf(maxOccurs); + }catch (Exception e) { + + } + } + }catch (Exception e) { + upperBound = Integer.MAX_VALUE; + } + + int inserted = numberFieldsMandatorySameKeyMap.get(entry.getKey()); + + logger.info("Field with key '" + entry.getKey() + "' has been found " + inserted + + " times and its lower bound is " + lowerBound + " and upper bound " + upperBound); + + if(inserted < lowerBound || inserted > upperBound) { + throw new BadRequestException("Field with key '" + entry.getKey() + + "' is mandatory, but it's not present among the provided fields or its cardinality is not respected ([min = " + + lowerBound + ", max=" + upperBound + "])."); + } + } + + // if there are no tags, throw an exception + if(tagsArrayOriginal.size() == 0) { + throw new BadRequestException("Please define at least one tag for this item!"); + } + + // sort validated custom fields and add to the extrasArrayUpdated json array + Collections.sort(validatedCustomFields); + + logger.debug("Sorted list of custom fields is " + validatedCustomFields); + + // add missing fields with no match (append them at the end, since no metadataIndex or categoryIndex was defined for them) + for(CustomField cf : customFields) + validatedCustomFields.add(cf); + + // convert back to json + for(CustomField customField : validatedCustomFields) { + ObjectNode jsonObj = mapper.createObjectNode(); + jsonObj.put(CKANPackage.EXTRAS_KEY_KEY, customField.getQualifiedKey()); + jsonObj.put(CKANPackage.EXTRAS_VALUE_KEY, customField.getValue()); + extrasArrayUpdated.add(jsonObj); + } + + // add metadata type field as last element + ObjectNode metadataTypeJSON = mapper.createObjectNode(); + metadataTypeJSON.put(CKANPackage.EXTRAS_KEY_KEY, metadataTypeCF.getKey()); + metadataTypeJSON.put(CKANPackage.EXTRAS_VALUE_KEY, metadataTypeCF.getValue()); + extrasArrayUpdated.add(metadataTypeJSON); + + // create groups + for(String title : groupsToCreateAfterValidation) { + try { + createGroupAsSysAdmin(title); + } catch(Exception e) { + logger.trace("Failed to create group with title " + title, e); + } + } + } + + objectNode.replace(CKANPackage.TAGS_KEY, tagsArrayOriginal); + objectNode.replace(CKANPackage.GROUPS_KEY, groupsArrayOriginal); + objectNode.replace(CKANPackage.EXTRAS_KEY, extrasArrayUpdated); + + return objectNode; + } + + } + + /** + * Retrieve an instance of the library for the scope + */ + public void createGroupAsSysAdmin(String title) throws Exception { + String sysAdminAPI = CKANUtility.getSysAdminAPI(); + CKANGroup ckanGroup = new CKANGroup(); + ckanGroup.setApiKey(sysAdminAPI); + ckanGroup.setName(CKANGroup.getCKANGroupName(title)); + try { + ckanGroup.read(); + } catch(WebApplicationException e) { + if(e.getResponse().getStatus() == Status.NOT_FOUND.getStatusCode()) { + ckanGroup.create(); + } else { + throw e; + } + } catch(Exception e) { + throw new InternalServerErrorException(e); + } finally { + try { + addUserToGroupAsSysAdmin(title); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + } + + public void addUserToGroupAsSysAdmin(String groupName) throws Exception { + String username = CKANUser.getCKANUsername(); + addUserToGroupAsSysAdmin(groupName, username); + } + + public void addUserToGroupAsSysAdmin(String groupName, String username) throws Exception { + String sysAdminAPI = CKANUtility.getSysAdminAPI(); + CKANUser ckanUser = new CKANUser(); + ckanUser.setApiKey(sysAdminAPI); + ckanUser.setName(username); + ckanUser.addToGroup(CKANGroup.getCKANGroupName(groupName)); + } + + /** + * Validate this field and generate a new value (or returns the same if there is nothing to update) + * @param metadataIndex + * @param categoryIndex + * @param customFields + * @param tagsArrayOriginal + * @param groupsArrayOriginal + * @param metadataField + * @param categories + * @param numberFieldsSameKeyMap + * @param fieldsMandatoryLowerBoundMap + * @param isApplication + * @return + * @throws Exception + */ + private List validateAgainstMetadataField(int metadataIndex, int categoryIndex, + List customFields, ArrayNode tagsArrayOriginal, ArrayNode groupsArrayOriginal, + MetadataField metadataField, List categories, + Map fieldsMandatoryLowerBoundMap, Map fieldsMandatoryUpperBoundMap, + Map numberFieldsMandatorySameKeyMap, List groupToCreate) throws Exception { + + List toReturn = new ArrayList(); + String metadataFieldName = metadataField.getCategoryFieldQName(); // get the qualified one, if any + int fieldsFoundWithThisKey = 0; + + Iterator iterator = customFields.iterator(); + while(iterator.hasNext()) { + CustomField cf = (CustomField) iterator.next(); + if(cf.getKey().equals(metadataFieldName)) { + validate(cf, metadataField); + fieldsFoundWithThisKey++; + cf.setIndexCategory(categoryIndex); + cf.setIndexMetadataField(metadataIndex); + checkAsGroup(cf, metadataField, groupsArrayOriginal, groupToCreate); + checkAsTag(cf, metadataField, tagsArrayOriginal); + toReturn.add(cf); + iterator.remove(); + } + } + + // in case of mandatory fields, keep track of the number of times they appear + if(metadataField.getMandatory()) { + // lower bound + int lowerBound = 1; + if(fieldsMandatoryLowerBoundMap.containsKey(metadataFieldName)) + lowerBound = fieldsMandatoryLowerBoundMap.get(metadataFieldName) + 1; + fieldsMandatoryLowerBoundMap.put(metadataFieldName, lowerBound); + + // upper bound + boolean hasVocabulary = metadataField.getVocabulary() != null; + int upperBound = hasVocabulary ? (metadataField.getVocabulary().isMultiSelection() + ? metadataField.getVocabulary().getVocabularyFields().size() + : 1) : 1; + + if(fieldsMandatoryUpperBoundMap.containsKey(metadataFieldName)) + upperBound += fieldsMandatoryUpperBoundMap.get(metadataFieldName); + + fieldsMandatoryUpperBoundMap.put(metadataFieldName, upperBound); + + // fields with this same key + int countPerFields = fieldsFoundWithThisKey; + if(numberFieldsMandatorySameKeyMap.containsKey(metadataFieldName)) + countPerFields += numberFieldsMandatorySameKeyMap.get(metadataFieldName); + numberFieldsMandatorySameKeyMap.put(metadataFieldName, countPerFields); + } + + // if there was no field with this key and it was not mandatory, just add an entry of the kind {"key": "key-value", "value" : ""}. + // Sometimes it is important to view the field as empty. + /* + if(fieldsFoundWithThisKey == 0 && !metadataField.getMandatory()) { + toReturn.add(new CustomField(metadataFieldName, "", -1, -1)); + } + */ + + return toReturn; + + } + + /** + * Check if a tag must be generated + * @param fieldToValidate + * @param metadataField + * @param tagsArrayOriginal + */ + private void checkAsTag(CustomField fieldToValidate, MetadataField metadataField, ArrayNode tagsArrayOriginal) { + MetadataTagging tagging = metadataField.getTagging(); + if(tagging != null) { + + String tag = ""; + + switch(tagging.getTaggingValue()) { + case onFieldName: + tag = metadataField.getFieldName(); + break; + case onValue: + tag = fieldToValidate.getValue(); + break; + case onFieldName_onValue: + tag = metadataField.getFieldName() + tagging.getSeparator() + fieldToValidate.getValue(); + break; + case onValue_onFieldName: + tag = fieldToValidate.getValue() + tagging.getSeparator() + metadataField.getFieldName(); + break; + default: + return; + } + + tag = tag.substring(0, MAX_TAG_CHARS > tag.length() ? tag.length() : MAX_TAG_CHARS); + logger.debug("Tag is " + tag); + + ObjectNode tagJSON = mapper.createObjectNode(); + tagJSON.put("name", tag); + tagJSON.put("display_name", tag); + tagsArrayOriginal.add(tagJSON); + + } + + } + + /** + * Check if a group must be generated + * @param fieldToValidate + * @param metadataField + * @param groupsArrayOriginal + * @param isApplication + * @throws Exception + */ + private void checkAsGroup(CustomField fieldToValidate, MetadataField metadataField, ArrayNode groupsArrayOriginal, + List groupToCreate) throws Exception { + + logger.debug("Custom field is " + fieldToValidate); + logger.debug("MetadataField field is " + metadataField); + logger.debug("JSONArray field is " + groupsArrayOriginal); + + MetadataGrouping grouping = metadataField.getGrouping(); + if(grouping != null) { + + boolean propagateUp = grouping.getPropagateUp(); + final Set groupNames = new HashSet(); + + switch(grouping.getGroupingValue()) { + case onFieldName: + groupNames.add(metadataField.getFieldName()); + break; + case onValue: + if(fieldToValidate.getValue() != null && !fieldToValidate.getValue().isEmpty()) + groupNames.add(fieldToValidate.getValue()); + break; + case onFieldName_onValue: + case onValue_onFieldName: + groupNames.add(metadataField.getFieldName()); + if(fieldToValidate.getValue() != null && !fieldToValidate.getValue().isEmpty()) + groupNames.add(fieldToValidate.getValue()); + break; + default: + return; + } + + for(String title : groupNames) { + String groupName = CKANGroup.getCKANGroupName(title); + logger.debug("Adding group to which add this item {}", groupName); + ObjectNode group = mapper.createObjectNode(); + group.put("name", groupName); + if(propagateUp) { + List parents = Validator.getGroupHierarchyNames(groupName); + for(String parent : parents) { + ObjectNode groupP = mapper.createObjectNode(); + groupP.put("name", parent); + groupsArrayOriginal.add(groupP); + } + } + groupsArrayOriginal.add(group); + } + + // force group creation if needed + if(grouping.getCreate()) { + for(String title : groupNames) + groupToCreate.add(title); + } + } + + } + + /** + * Validate the single field + * @param fieldToValidate + * @param metadataField + * @param isFirst + * @return + * @throws Exception + */ + private void validate(CustomField fieldToValidate, MetadataField metadataField) throws Exception { + + DataType dataType = metadataField.getDataType(); + String regex = metadataField.getValidator() != null ? metadataField.getValidator().getRegularExpression() + : null; + boolean hasControlledVocabulary = false; + MetadataVocabulary metadataVocabulary = metadataField.getVocabulary(); + if(metadataVocabulary!=null) { + List vocabularyFields = metadataVocabulary.getVocabularyFields(); + if(vocabularyFields!=null && vocabularyFields.size()>0) { + hasControlledVocabulary = true; + } + } + String value = fieldToValidate.getValue(); + String key = fieldToValidate.getKey(); + String defaultValue = metadataField.getDefaultValue(); + + // replace key by prepending the qualified name of the category, if needed + fieldToValidate.setQualifiedKey(metadataField.getCategoryFieldQName()); + + if((value == null || value.isEmpty())) + if(metadataField.getMandatory() || hasControlledVocabulary) + throw new BadRequestException("Mandatory field with name '" + key + + "' doesn't have a value but it is mandatory or has a controlled vocabulary!"); + else { + if(defaultValue != null && !defaultValue.isEmpty()) { + value = defaultValue; + fieldToValidate.setValue(defaultValue); + } + return; // there is no need to check other stuff + } + + switch(dataType) { + + case String: + case Text: + + if(regex != null && !value.matches(regex)) + throw new BadRequestException("Field with key '" + key + + "' doesn't match the provided regular expression (" + regex + ")!"); + + if(hasControlledVocabulary) { + + List valuesVocabulary = metadataField.getVocabulary().getVocabularyFields(); + + if(valuesVocabulary == null || valuesVocabulary.isEmpty()) + return; + + boolean match = false; + for(String valueVocabulary : valuesVocabulary) { + match = value.equals(valueVocabulary); + if(match) + break; + } + + if(!match) + throw new BadRequestException("Field with key '" + key + "' has a value '" + value + + "' but it doesn't match any of the vocabulary's values (" + valuesVocabulary + ")!"); + + } + + break; + case Time: + + if(!isValidDate(value)) + throw new BadRequestException("Field with key '" + key + "' doesn't seem a valid time!"); + + break; + case Time_Interval: + + String[] timeValues = value.split("/"); + for(int i = 0; i < timeValues.length; i++) { + String time = timeValues[i]; + if(!isValidDate(time)) + throw new BadRequestException( + "Field with key '" + key + "' doesn't seem a valid time interval!"); + } + + break; + case Times_ListOf: + + String[] timeIntervals = value.split(","); + for(int i = 0; i < timeIntervals.length; i++) { + String[] timeIntervalValues = timeIntervals[i].split("/"); + if(timeIntervalValues.length > 2) + throw new BadRequestException( + "Field with key '" + key + "' doesn't seem a valid list of times!"); + for(i = 0; i < timeIntervalValues.length; i++) { + String time = timeIntervalValues[i]; + if(!isValidDate(time)) + throw new BadRequestException( + "Field with key '" + key + "' doesn't seem a valid list of times!"); + } + } + + break; + case Boolean: + + if(value.equalsIgnoreCase("true") || value.equalsIgnoreCase("false")) { + + } else + throw new BadRequestException("Field with key '" + key + "' doesn't seem a valid boolean value!"); + + break; + case Number: + + if(!NumberUtils.isNumber(value)) + throw new BadRequestException("Field's value with key '" + key + "' is not a valid number!"); + + break; + case GeoJSON: + + try { + new com.fasterxml.jackson.databind.ObjectMapper().readValue(fieldToValidate.getValue(), GeoJsonObject.class); + } catch(Exception e) { + throw new BadRequestException("GeoJSON field with key '" + key + "' seems not valid!"); + } + + break; + default: + break; + } + + } + + /** + * Validate a time date against a formatter + * @param value + * @param formatter + * @return + */ + private static boolean isValidDate(String value) { + + try { + DATE_HOUR_MINUTES.parse(value); + return true; + } catch(Exception e) { + logger.debug("failed to parse date with hours and minutes, trying the other one"); + try { + DATE_SIMPLE.parse(value); + return true; + } catch(Exception e2) { + logger.warn("failed to parse date with simple format, returning false"); + return false; + } + } + + } + + /** + * Get the group hierarchy + * @param groupName + * @throws Exception + */ + public static List getGroupHierarchyNames(String groupName) throws Exception { + CKANGroup ckanGroup = new CKANGroup(); + ckanGroup.setName(groupName); + return ckanGroup.getGroups(); + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKAN.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKAN.java new file mode 100644 index 0000000..f9372a5 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKAN.java @@ -0,0 +1,366 @@ +package org.gcube.gcat.persistence.ckan; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; + +import org.gcube.com.fasterxml.jackson.core.JsonProcessingException; +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.NullNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.gxhttp.request.GXHTTPStringRequest; +import org.gcube.gcat.utils.HTTPUtility; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class CKAN { + + private static final Logger logger = LoggerFactory.getLogger(CKAN.class); + + protected static final String ID_KEY = "id"; + protected static final String TITLE_KEY = "title"; + protected static final String NAME_KEY = "name"; + + protected static final String ERROR_KEY = "error"; + protected static final String ERROR_TYPE_KEY = "__type"; + protected static final String MESSAGE_KEY = "message"; + protected static final String OWNER_ORG_KEY = "owner_org"; + + protected static final String RESULT_KEY = "result"; + protected static final String SUCCESS_KEY = "success"; + + public static final String LIMIT_KEY = "limit"; + public static final String OFFSET_KEY = "offset"; + + protected static final String NOT_FOUND_ERROR = "Not Found Error"; + protected static final String AUTHORIZATION_ERROR = "Authorization Error"; + protected static final String VALIDATION_ERROR = "Validation Error"; + + // api rest path CKAN + public final static String CKAN_API_PATH = "/api/3/action/"; + + // ckan header authorization + public final static String AUTH_CKAN_HEADER = HttpHeaders.AUTHORIZATION; + + public final static String NAME_REGEX = "^[a-z0-9_\\\\-]{2,100}$"; + + protected String LIST; + protected String CREATE; + protected String READ; + protected String UPDATE; + protected String PATCH; + protected String DELETE; + protected String PURGE; + + protected final ObjectMapper mapper; + + protected String name; + protected String apiKey; + + protected JsonNode result; + + protected String nameRegex; + + protected UriInfo uriInfo; + + public void setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + } + + public String getApiKey() { + if(apiKey == null) { + try { + return CKANUtility.getApiKey(); + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public ObjectMapper getMapper() { + return mapper; + } + + public JsonNode getJsonNodeResult() { + return result; + } + + protected CKAN() { + try { + this.mapper = new ObjectMapper(); + this.nameRegex = CKAN.NAME_REGEX; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected JsonNode getAsJsonNode(String json) { + try { + return mapper.readTree(json); + } catch(IOException e) { + throw new BadRequestException(e); + } + } + + + /** + * Validate the CKAN response and return the + * @param json + * @return the validated response as JsonNode + */ + protected JsonNode validateCKANResponse(String json) { + JsonNode jsonNode = getAsJsonNode(json); + if(jsonNode.get(SUCCESS_KEY).asBoolean()) { + return jsonNode.get(RESULT_KEY); + } else { + try { + JsonNode error = jsonNode.get(ERROR_KEY); + + String errorType = error.get(ERROR_TYPE_KEY).asText(); + + if(errorType.compareTo(VALIDATION_ERROR) == 0) { + throw new BadRequestException(getAsString(error)); + } + + String message = error.get(MESSAGE_KEY).asText(); + + if(errorType.compareTo(NOT_FOUND_ERROR) == 0) { + throw new NotFoundException(message); + } + + if(errorType.compareTo(AUTHORIZATION_ERROR) == 0) { + throw new ForbiddenException(message); + } + + // TODO parse more cases + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new BadRequestException(json); + } + throw new BadRequestException(json); + } + } + + protected String getAsString(JsonNode node) { + try { + String json = mapper.writeValueAsString(node); + return json; + } catch(JsonProcessingException e) { + throw new InternalServerErrorException(e); + } + } + + protected JsonNode checkName(JsonNode jsonNode) { + try { + String gotName = jsonNode.get(NAME_KEY).asText(); + if(!gotName.matches(nameRegex)) { + throw new BadRequestException( + "The 'name' must be between 2 and 100 characters long and contain only lowercase alphanumeric characters, '-' and '_'. You can validate your name using the regular expression : " + + NAME_REGEX); + } + + if(name == null) { + name = gotName; + } + + if(gotName != null && gotName.compareTo(name) != 0) { + String error = String.format( + "The name (%s) does not match with the '%s' contained in the provided content (%s).", name, + NAME_KEY, gotName); + throw new BadRequestException(error); + } + return jsonNode; + } catch(BadRequestException e) { + throw e; + } catch(Exception e) { + throw new BadRequestException("Unable to obtain a correct 'name' from the provided content"); + } + } + + protected JsonNode checkName(String json) { + JsonNode jsonNode = getAsJsonNode(json); + checkName(jsonNode); + return jsonNode; + } + + protected JsonNode createJsonNodeWithID(String id) { + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put(ID_KEY, id); + return objectNode; + } + + protected JsonNode createJsonNodeWithNameAsID() { + return createJsonNodeWithID(name); + } + + protected Map getMapWithNameAsID() { + return getMapWithID(name); + } + + protected Map getMapWithID(String id) { + Map map = new HashMap<>(); + map.put(ID_KEY, id); + return map; + } + + protected GXHTTPStringRequest getGXHTTPStringRequest(String path, boolean post) + throws UnsupportedEncodingException { + String catalogueURL = CKANUtility.getCkanURL(); + + GXHTTPStringRequest gxhttpStringRequest = HTTPUtility.createGXHTTPStringRequest(catalogueURL, path, post); + gxhttpStringRequest.isExternalCall(true); + gxhttpStringRequest.header(AUTH_CKAN_HEADER, getApiKey()); + + return gxhttpStringRequest; + } + + protected String getResultAsString(HttpURLConnection httpURLConnection) throws IOException { + int responseCode = httpURLConnection.getResponseCode(); + if(responseCode >= Status.BAD_REQUEST.getStatusCode()) { + Status status = Status.fromStatusCode(responseCode); + switch (status) { + case NOT_FOUND: + throw new NotFoundException(); + + default: + break; + } + InputStream inputStream = httpURLConnection.getErrorStream(); + StringBuilder result = HTTPUtility.getStringBuilder(inputStream); + logger.trace(result.toString()); + try { + JsonNode jsonNode = getAsJsonNode(result.toString()); + JsonNode error = jsonNode.get(ERROR_KEY); + throw new WebApplicationException(getAsString(error), status); + }catch (WebApplicationException e) { + throw e; + }catch (Exception e) { + throw new WebApplicationException(result.toString(), status); + } + } + InputStream inputStream = httpURLConnection.getInputStream(); + String ret = HTTPUtility.getStringBuilder(inputStream).toString(); + logger.trace("Got Respose is {}", ret); + return ret; + } + + protected String getResultAndValidate(HttpURLConnection httpURLConnection) throws IOException { + String ret = getResultAsString(httpURLConnection); + logger.trace("Got Respose is {}", ret); + result = validateCKANResponse(ret); + if(result instanceof NullNode) { + result = mapper.createObjectNode(); + } + return getAsString(result); + } + + protected String sendGetRequest(String path, Map parameters) { + try { + logger.debug("Going to send GET request with parameters {}", parameters); + GXHTTPStringRequest gxhttpStringRequest = getGXHTTPStringRequest(path, false); + gxhttpStringRequest.queryParams(parameters); + HttpURLConnection httpURLConnection = gxhttpStringRequest.get(); + return getResultAndValidate(httpURLConnection); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected String sendPostRequest(String path, String body) { + try { + logger.debug("Going to send POST request with body {}", body); + GXHTTPStringRequest gxhttpStringRequest = getGXHTTPStringRequest(path, true); + HttpURLConnection httpURLConnection = gxhttpStringRequest.post(body); + return getResultAndValidate(httpURLConnection); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected String sendPostRequest(String path, JsonNode jsonNode) { + return sendPostRequest(path, getAsString(jsonNode)); + } + + public String list(int limit, int offset) { + Map parameters = new HashMap<>(); + if(limit > 0) { + parameters.put(LIMIT_KEY, String.valueOf(limit)); + } + if(offset >= 0) { + parameters.put(OFFSET_KEY, String.valueOf(offset)); + } + return sendGetRequest(LIST, parameters); + } + + public String create(String json) { + return sendPostRequest(CREATE, json); + } + + public String read() { + return sendGetRequest(READ, getMapWithNameAsID()); + } + + public String update(String json) { + checkName(json); + return sendPostRequest(UPDATE, json); + } + + public String patch(String json) { + JsonNode jsonNode = checkName(json); + ObjectNode objectNode = ((ObjectNode) jsonNode); + objectNode.put(ID_KEY, name); + objectNode.remove(NAME_KEY); + return sendPostRequest(PATCH, objectNode); + } + + protected void delete() { + sendPostRequest(DELETE, createJsonNodeWithNameAsID()); + } + + public void delete(boolean purge) { + if(purge) { + purge(); + } else { + delete(); + } + } + + public void purge() { + sendPostRequest(PURGE, createJsonNodeWithNameAsID()); + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANGroup.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANGroup.java new file mode 100644 index 0000000..6fe1a8d --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANGroup.java @@ -0,0 +1,108 @@ +package org.gcube.gcat.persistence.ckan; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; + +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANGroup extends CKAN { + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.group_list + public static final String GROUP_LIST = CKAN.CKAN_API_PATH + "group_list"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.group_create + public static final String GROUP_CREATE = CKAN.CKAN_API_PATH + "group_create"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.group_show + public static final String GROUP_SHOW = CKAN.CKAN_API_PATH + "group_show"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.group_update + public static final String GROUP_UPDATE = CKAN.CKAN_API_PATH + "group_update"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.group_patch + public static final String GROUP_PATCH = CKAN.CKAN_API_PATH + "group_patch"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.group_delete + public static final String GROUP_DELETE = CKAN.CKAN_API_PATH + "group_delete"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.group_purge + public static final String GROUP_PURGE = CKAN.CKAN_API_PATH + "group_purge"; + + public static final String GROUPS_KEY = "groups"; + + public CKANGroup() { + super(); + LIST = GROUP_LIST; + CREATE = GROUP_CREATE; + READ = GROUP_SHOW; + UPDATE = GROUP_UPDATE; + PATCH = GROUP_PATCH; + DELETE = GROUP_DELETE; + PURGE = GROUP_PURGE; + } + + public static String fromGroupTitleToName(String groupTitle) { + if(groupTitle == null) + return null; + + String regexGroupTitleTransform = "[^A-Za-z0-9-]"; + String modified = groupTitle.trim().replaceAll(regexGroupTitleTransform, "-").replaceAll("-+", "-").toLowerCase(); + + if(modified.startsWith("-")) + modified = modified.substring(1); + if(modified.endsWith("-")) + modified = modified.substring(0, modified.length() - 1); + + return modified; + + } + + public static String getCKANGroupName(String name) { + return CKANGroup.fromGroupTitleToName(name); + } + + public String create() throws WebApplicationException { + try { + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put(NAME_KEY, CKANGroup.getCKANGroupName(name)); + objectNode.put("title", name); + objectNode.put("display_name", name); + objectNode.put("description", ""); + return super.create(mapper.writeValueAsString(objectNode)); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + public List getGroups() { + if(result == null) { + read(); + } + List groups = new ArrayList(); + if(result.has(GROUPS_KEY)) { + JsonNode jsonNode = result.get(GROUPS_KEY); + if(jsonNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) jsonNode; + if(arrayNode.size() > 0) { + Iterator iterator = arrayNode.iterator(); + while(iterator.hasNext()) { + groups.add(iterator.next().asText()); + } + } + } + } + return groups; + } + + public int count() { + list(100000, 0); + ArrayNode arrayNode = (ArrayNode) result; + return arrayNode.size(); + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANLicense.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANLicense.java new file mode 100644 index 0000000..baffab7 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANLicense.java @@ -0,0 +1,49 @@ +package org.gcube.gcat.persistence.ckan; + +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANLicense extends CKAN { + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.license_list + public static final String LICENSES_LIST = CKAN.CKAN_API_PATH + "license_list"; + + public CKANLicense() { + super(); + LIST = LICENSES_LIST; + } + + protected static ArrayNode getLicenses() { + CKANLicense ckanLicense = new CKANLicense(); + ckanLicense.list(-1, -1); + ArrayNode arrayNode = (ArrayNode) ckanLicense.getJsonNodeResult(); + return arrayNode; + } + + public static boolean checkLicenseId(String licenseId) throws Exception { + return checkLicenseId(getLicenses(), licenseId); + } + + // TODO Use a Cache + protected static boolean checkLicenseId(ArrayNode arrayNode, String licenseId) throws Exception { + try { + for(JsonNode jsonNode : arrayNode) { + try { + String id = jsonNode.get(ID_KEY).asText(); + if(id.compareTo(licenseId) == 0) { + return true; + } + } catch(Exception e) { + + } + } + return false; + } catch(Exception e) { + throw e; + } + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANOrganization.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANOrganization.java new file mode 100644 index 0000000..aa79ee8 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANOrganization.java @@ -0,0 +1,86 @@ +package org.gcube.gcat.persistence.ckan; + +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.scope.impl.ScopeBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANOrganization extends CKAN { + + private static Logger logger = LoggerFactory.getLogger(CKANOrganization.class); + + // CKAN Connector sanitize the Organization name as following + //organizationName.replaceAll(" ", "_").replace(".", "_").toLowerCase() + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.organization_list + public static final String ORGANIZATION_LIST = CKAN.CKAN_API_PATH + "organization_list"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.organization_create + public static final String ORGANIZATION_CREATE = CKAN.CKAN_API_PATH + "organization_create"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.organization_show + public static final String ORGANIZATION_SHOW = CKAN.CKAN_API_PATH + "organization_show"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.organization_update + public static final String ORGANIZATION_UPDATE = CKAN.CKAN_API_PATH + "organization_update"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.organization_patch + public static final String ORGANIZATION_PATCH = CKAN.CKAN_API_PATH + "organization_patch"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.organization_delete + public static final String ORGANIZATION_DELETE = CKAN.CKAN_API_PATH + "organization_delete"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.organization_purge + public static final String ORGANIZATION_PURGE = CKAN.CKAN_API_PATH + "organization_purge"; + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.organization_member_create + public static final String ORGANIZATION_MEMBER_CREATE = CKAN.CKAN_API_PATH + "organization_member_create"; + + // https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.organization_list_for_user + public static final String ORGANIZATION_LIST_FOR_USER = CKAN.CKAN_API_PATH + "organization_list_for_user"; + + protected static final String USERNAME_KEY = "username"; + protected static final String ROLE_KEY = "role"; + + public CKANOrganization() { + super(); + LIST = ORGANIZATION_LIST; + CREATE = ORGANIZATION_CREATE; + READ = ORGANIZATION_SHOW; + UPDATE = ORGANIZATION_UPDATE; + PATCH = ORGANIZATION_PATCH; + DELETE = ORGANIZATION_DELETE; + PURGE = ORGANIZATION_PURGE; + } + + protected static final String ORGANIZATION_PERMISSION_KEY = "permission"; + protected static final String ORGANIZATION_PERMISSION_VALUE_READ = "read"; + + public void addUserToOrganisation(String gCubeUsername, String role) { + String ckanUsername = CKANUser.getCKANUsername(gCubeUsername); + + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put(ID_KEY, name); + objectNode.put(USERNAME_KEY, ckanUsername); + objectNode.put(ROLE_KEY, role); + sendPostRequest(ORGANIZATION_MEMBER_CREATE, getAsString(objectNode)); + logger.debug("User {} successfully added to Organisation {} with role {}", ckanUsername, name, role); + + } + + public static String getCKANOrganizationName() { + String context = SecretManagerProvider.instance.get().getContext(); + return getCKANOrganizationName(context); + } + + public static String getCKANOrganizationName(String context) { + ScopeBean scopeBean = new ScopeBean(context); + return scopeBean.name().toLowerCase(); + } + + public int count() { + list(100000, 0); + ArrayNode arrayNode = (ArrayNode) result; + return arrayNode.size(); + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackage.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackage.java new file mode 100644 index 0000000..87bac0a --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackage.java @@ -0,0 +1,1678 @@ +package org.gcube.gcat.persistence.ckan; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MultivaluedMap; + +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.scope.impl.ScopeBean; +import org.gcube.common.scope.impl.ScopeBean.Type; +import org.gcube.gcat.api.GCatConstants; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.api.moderation.CMItemStatus; +import org.gcube.gcat.api.moderation.CMItemVisibility; +import org.gcube.gcat.api.moderation.Moderated; +import org.gcube.gcat.api.moderation.ModerationContent; +import org.gcube.gcat.api.roles.Role; +import org.gcube.gcat.configuration.CatalogueConfigurationFactory; +import org.gcube.gcat.moderation.thread.ModerationThread; +import org.gcube.gcat.oldutils.Validator; +import org.gcube.gcat.persistence.ckan.cache.CKANUserCache; +import org.gcube.gcat.profile.MetadataUtility; +import org.gcube.gcat.social.SocialPost; +import org.gcube.gcat.utils.URIResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANPackage extends CKAN implements Moderated { + + private static final Logger logger = LoggerFactory.getLogger(CKANPackage.class); + + /* + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.package_list + public static final String ITEM_LIST = CKAN.CKAN_API_PATH + "package_list"; + */ + // see https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search + public static final String ITEM_LIST = CKAN.CKAN_API_PATH + "package_search"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.package_create + public static final String ITEM_CREATE = CKAN.CKAN_API_PATH + "package_create"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.package_show + public static final String ITEM_SHOW = CKAN.CKAN_API_PATH + "package_show"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.package_update + public static final String ITEM_UPDATE = CKAN.CKAN_API_PATH + "package_update"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.package_patch + public static final String ITEM_PATCH = CKAN.CKAN_API_PATH + "package_patch"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.package_delete + public static final String ITEM_DELETE = CKAN.CKAN_API_PATH + "package_delete"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.dataset_purge + public static final String ITEM_PURGE = CKAN.CKAN_API_PATH + "dataset_purge"; + + + // limit in https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search + public static final String ROWS_KEY = "rows"; + // offset in https://docs.ckan.org/en/latest/api/index.html#ckan.logic.action.get.package_search + public static final String START_KEY = "start"; + + protected static final String ORGANIZATION_FILTER_TEMPLATE = GCatConstants.ORGANIZATION_PARAMETER + ":%s"; + + protected static final String ORGANIZATION_REGEX = GCatConstants.ORGANIZATION_PARAMETER + ":[a-zA-Z0-9_\\-\"]*"; + + protected static final Pattern ORGANIZATION_REGEX_PATTERN; + + static { + ORGANIZATION_REGEX_PATTERN = Pattern.compile(ORGANIZATION_REGEX); + } + + protected static final String LICENSE_KEY = "license_id"; + + protected static final String EXTRAS_ITEM_URL_KEY = "Item URL"; + + protected static final String AUTHOR_KEY = "author"; + protected static final String AUTHOR_EMAIL_KEY = "author_email"; + + protected static final String MAINTAINER_KEY = "maintainer"; + protected static final String MAINTAINER_EMAIL_KEY = "maintainer_email"; + + protected static final String OWNER_ORG_KEY = "owner_org"; + protected static final String RESOURCES_KEY = "resources"; + protected static final String TITLE_KEY = "title"; + + public static final String EXTRAS_KEY = "extras"; + public static final String EXTRAS_KEY_KEY = "key"; + public static final String EXTRAS_KEY_VALUE_SYSTEM_TYPE = "system:type"; + public static final String EXTRAS_VALUE_KEY = "value"; + + // The 'results' array is included in the 'result' object for package_search + private static final String RESULTS_KEY = "results"; + + protected static final String PRIVATE_KEY = "private"; + protected static final String SEARCHABLE_KEY = "searchable"; + protected static final String CAPACITY_KEY = "capacity"; + + protected static final String CM_STATUS_QUERY_FILTER_KEY = "extras_systemcm_item_status"; + + protected static final String INCLUDE_PRIVATE_KEY = "include_private"; + // protected static final String INCLUDE_DRAFTS_KEY = "include_drafts"; + + public static final String GROUPS_KEY = "groups"; + public static final String TAGS_KEY = "tags"; + + protected final List managedResources; + + protected String itemID; + protected String itemTitle; + protected String itemURL; + + protected final CKANUser ckanUser; + + protected CatalogueConfiguration configuration; + + protected boolean updateOperation; + + protected ModerationThread moderationThread; + + /** + * By default extra properties used for moderation are removed + * from the item representation. + * So that, the default value of this field is false. + * + * A Catalogue-Manager can request to keep such properties + * for debugging purposes. + * + */ + protected boolean keepModerationExtraProperties; + + public CKANPackage() { + this(CatalogueConfigurationFactory.getInstance()); + } + + protected CKANPackage(CatalogueConfiguration configuration) { + super(); + + this.LIST = ITEM_LIST; + this.CREATE = ITEM_CREATE; + this.READ = ITEM_SHOW; + this.UPDATE = ITEM_UPDATE; + this.PATCH = ITEM_PATCH; + this.DELETE = ITEM_DELETE; + this.PURGE = ITEM_PURGE; + + this.managedResources = new ArrayList(); + + this.configuration = configuration; + + this.ckanUser = CKANUserCache.getCurrrentCKANUser(); + + this.updateOperation = false; + this.keepModerationExtraProperties = true; + } + + public void setKeepModerationExtraProperties(boolean keepModerationExtraProperties) { + if(ckanUser.getRole().ordinal()>=Role.MANAGER.ordinal() && keepModerationExtraProperties) { + this.keepModerationExtraProperties = keepModerationExtraProperties; + } + + } + + //protected CKANOrganization checkGotOrganization(String gotOrganization) throws ForbiddenException { + protected void checkGotOrganization(String gotOrganization) throws ForbiddenException { + if(!configuration.getSupportedOrganizations().contains(gotOrganization)) { + String error = String.format( + "IS Configuration does not allow to publish in %s organizations. Allowed organization are: %s", + gotOrganization, configuration.getSupportedOrganizations()); + throw new ForbiddenException(error); + } + + /* + * It seem not needed. Add a cache if we want this check + * + * CKANOrganization ckanOrganization = new CKANOrganization(); + * ckanOrganization.setName(gotOrganization); + * ckanOrganization.read(); + * + * return ckanOrganization; + * + */ + + } + + /* + protected CKANOrganization getPublishingOrganization(ObjectNode objectNode) throws ForbiddenException { + CKANOrganization ckanOrganization = null; + if(objectNode.has(OWNER_ORG_KEY)) { + String gotOrganizationName = objectNode.get(OWNER_ORG_KEY).asText(); + ckanOrganization = checkGotOrganization(gotOrganizationName); + } + + if(ckanOrganization == null) { + // owner organization must be specified if the token belongs to a VRE + String organizationFromContext = configuration.getDefaultOrganization(); + ckanOrganization = checkGotOrganization(organizationFromContext); + objectNode.put(OWNER_ORG_KEY, organizationFromContext); + } + + return ckanOrganization; + } + */ + + protected void getPublishingOrganization(ObjectNode objectNode) throws ForbiddenException { + if(objectNode.has(OWNER_ORG_KEY)) { + String gotOrganizationName = objectNode.get(OWNER_ORG_KEY).asText(); + checkGotOrganization(gotOrganizationName); + }else { + String organizationFromContext = configuration.getDefaultOrganization(); + checkGotOrganization(organizationFromContext); + objectNode.put(OWNER_ORG_KEY, organizationFromContext); + } + } + + public ObjectNode checkBaseInformation(String json) throws Exception { + return checkBaseInformation(json, false); + } + + public JsonNode cleanResult(JsonNode jsonNode) { + if(jsonNode.has(OWNER_ORG_KEY)) { + ((ObjectNode) jsonNode).remove(OWNER_ORG_KEY); + } + + // Removing all Content Moderation Keys + if(jsonNode.has(EXTRAS_KEY)) { + if(ckanUser.getRole().ordinal()>=Role.MANAGER.ordinal() && keepModerationExtraProperties) { + logger.trace("The user is a {} which requested to keep Moderation extra properties.", ckanUser.getRole()); + }else { + ArrayNode extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); + // It is not possible to remove the element of an array while iterating it. + // We need to create a new array only with valid elements; + ArrayNode newExtras = mapper.createArrayNode(); + boolean foundOne = false; + for(int i=0; i addFieldsFilters(Map parameters, String... requiredFields){ + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("["); + stringBuffer.append("'"); + stringBuffer.append(ID_KEY); + stringBuffer.append("'"); + stringBuffer.append(","); + stringBuffer.append("'"); + stringBuffer.append(NAME_KEY); + stringBuffer.append("'"); + for(String requiredField : requiredFields) { + if(requiredField!=null && requiredField.compareTo("")!=0) { + stringBuffer.append(","); + stringBuffer.append("'"); + stringBuffer.append(requiredField); + stringBuffer.append("'"); + } + } + stringBuffer.append("]"); + parameters.put("fl", stringBuffer.toString()); + return parameters; + } + + protected Map getListingParameters(int limit, int offset, String... requiredFields) { + Map parameters = new HashMap<>(); + if(limit <= 0) { + // According to CKAN documentation + // the number of matching rows to return. There is a hard limit of 1000 datasets per query. + // see https://docs.ckan.org/en/2.6/api/index.html#ckan.logic.action.get.package_search + limit = 1000; + } + parameters.put(ROWS_KEY, String.valueOf(limit)); + + if(offset < 0) { + offset = 0; + } + parameters.put(START_KEY, String.valueOf(offset)); + // parameters.put(START_KEY, String.valueOf(pageNumber * limit)); + + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + parameters = checkListParameters(queryParameters, parameters); + + parameters = addFieldsFilters(parameters, requiredFields); + + parameters = addModerationStatusFilter(parameters); + + return parameters; + } + + + protected void reuseInstance() { + this.name = null; + this.result = null; + this.itemID = null; + this.itemURL = null; + this.itemTitle = null; + } + + /** + * @param purge indicate if the item + * @return the name list of deleted items + */ + public String deleteAll(boolean purge){ + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + if(queryParameters.containsKey(GCatConstants.OWN_ONLY_QUERY_PARAMETER)) { + if(ckanUser.getRole().ordinal() < Role.ADMIN.ordinal()) { + queryParameters.remove(GCatConstants.OWN_ONLY_QUERY_PARAMETER); + queryParameters.add(GCatConstants.OWN_ONLY_QUERY_PARAMETER, Boolean.TRUE.toString()); + } + }else { + queryParameters.add(GCatConstants.OWN_ONLY_QUERY_PARAMETER, Boolean.TRUE.toString()); + } + + int limit = 25; + int offset = 0; + Map parameters = getListingParameters(limit,offset, "resources"); + ObjectNode objectNode = mapper.createObjectNode(); + + ArrayNode deleted = mapper.createArrayNode(); + ArrayNode notDeleted = mapper.createArrayNode(); + + sendGetRequest(LIST, parameters); + ArrayNode results = (ArrayNode) result.get(RESULTS_KEY); + + Set notDeletedSet = new HashSet<>(); + + while(results.size()>0) { + int alreadyTriedAndNotDeletedAgain = 0; + for(JsonNode node : results) { + try { + this.reuseInstance(); + this.result = node; + this.name = node.get(NAME_KEY).asText(); + this.itemID = node.get(ID_KEY).asText(); + delete(purge); + deleted.add(name); + if(notDeletedSet.contains(name)) { + notDeletedSet.remove(name); + } + try { + Thread.sleep(TimeUnit.MILLISECONDS.toMillis(300)); + } catch (InterruptedException e) { + + } + } catch(Exception e) { + try { + if(name!=null) { + if(notDeletedSet.contains(name)) { + alreadyTriedAndNotDeletedAgain++; + }else { + notDeleted.add(name); + notDeletedSet.add(name); + } + logger.error("Error while trying to delete item with name {}", name); + }else { + logger.error("Unable to get the name of {}.",mapper.writeValueAsString(node)); + } + } catch(Exception ex) { + logger.error("", ex); + } + } + } + + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(5)); + } catch (InterruptedException e) { + + } + if(purge) { + setApiKey(CKANUtility.getApiKey()); + } + + if(limit==alreadyTriedAndNotDeletedAgain) { + offset++; + parameters = getListingParameters(limit,offset, "resources"); + } + + sendGetRequest(LIST, parameters); + results = (ArrayNode) result.get(RESULTS_KEY); + + } + + if(notDeleted.size()!=notDeletedSet.size()) { + notDeleted = mapper.createArrayNode(); + for(String name : notDeletedSet) { + notDeleted.add(name); + } + } + + objectNode.set("deleted", deleted); + objectNode.set("failed", notDeleted); + + return getAsString(objectNode); + } + + public String list(Map parameters) { + logger.trace("Going to query Ckan with the following parameters {}", parameters); + + sendGetRequest(LIST, parameters); + + ArrayNode results = (ArrayNode) result.get(RESULTS_KEY); + + boolean allFields = false; + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + if(queryParameters.containsKey(GCatConstants.ALL_FIELDS_QUERY_PARAMETER)) { + allFields = Boolean.parseBoolean(queryParameters.get(GCatConstants.ALL_FIELDS_QUERY_PARAMETER).get(0)); + } + + if(allFields) { + return getAsString(results); + } + + ArrayNode arrayNode = mapper.createArrayNode(); + for(JsonNode node : results) { + try { + String name = node.get(NAME_KEY).asText(); + arrayNode.add(name); + } catch(Exception e) { + try { + logger.error("Unable to get the name of {}. The Item will not be included in the result", + mapper.writeValueAsString(node)); + } catch(Exception ex) { + logger.error("", ex); + } + } + } + + return getAsString(arrayNode); + } + + @Override + public String list(int limit, int offset) { + Map parameters = getListingParameters(limit, offset); + return list(parameters); + } + + public int count() { + Map parameters = getListingParameters(1, 0); + + sendGetRequest(LIST, parameters); + + int count = result.get(GCatConstants.COUNT_KEY).asInt(); + return count; + } + + protected Set checkOrganizationFilter(String q) { + Matcher m = ORGANIZATION_REGEX_PATTERN.matcher(q); + + Set matches = new HashSet<>(); + while(m.find()) { + matches.add(q.substring(m.start(), m.end()).replace(GCatConstants.ORGANIZATION_PARAMETER+":", "")); + } + + return matches; + } + + protected static String[] allowedListQueryParameters = new String[] {"fq", "fq_list", "sort", + /* "facet", "facet.mincount", "facet.limit", "facet.field", */ + "include_drafts", "include_private", "ext_bbox"}; + + protected String getFilterForOrganizations() { + StringWriter stringWriter = new StringWriter(); + /* + * This generated something like: + * " (organization:orgfortesting OR organization:prevre OR organization:data_inrae) " + */ + int i=1; + stringWriter.append(" ("); + for(String organizationName : configuration.getSupportedOrganizations()) { + stringWriter.append(String.format(GCatConstants.ORGANIZATION_FILTER_TEMPLATE, organizationName)); + if(i!=configuration.getSupportedOrganizations().size()) { + // Please note that an item can only belong to a single organization. + // Hence the query must put supported organizations in OR. + stringWriter.append(" OR "); + } + i++; + } + stringWriter.append(") "); + return stringWriter.toString(); + } + + protected Map checkListParameters(MultivaluedMap queryParameters, + Map parameters) { + + String q = null; + if(queryParameters.containsKey(GCatConstants.Q_KEY)) { + q = queryParameters.getFirst(GCatConstants.Q_KEY); + } + + if(q != null) { + Set organizations = checkOrganizationFilter(q); + + if(organizations.size()==0) { + // Adding organization filter to q + String filter = getFilterForOrganizations(); + q = String.format("%s AND %s", q, filter); + }else { + organizations.removeAll(configuration.getSupportedOrganizations()); + if(organizations.size()>0) { + String error = String.format("It is not possible to query the following organizations %s. Supported organization in this context are %s", organizations.toString(), configuration.getSupportedOrganizations().toString()); + throw new ForbiddenException(error); + } + } + + } else { + String filter = getFilterForOrganizations(); + q = filter; + } + + if(queryParameters.containsKey(GCatConstants.OWN_ONLY_QUERY_PARAMETER)) { + if(!queryParameters.get(GCatConstants.OWN_ONLY_QUERY_PARAMETER).isEmpty() && Boolean.parseBoolean(queryParameters.get(GCatConstants.OWN_ONLY_QUERY_PARAMETER).get(0))) { + String filter = String.format("%s:%s", AUTHOR_EMAIL_KEY, ckanUser.getEMail()); + q = String.format("%s AND %s", q, filter); + } + } + + logger.trace("Querying Ckan with {}={}",GCatConstants.Q_KEY, q); + parameters.put(GCatConstants.Q_KEY, q); + + for(String key : allowedListQueryParameters) { + if(queryParameters.containsKey(key)) { + parameters.put(key, queryParameters.getFirst(key)); + } + } + + // parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true)); + + // By default not including draft + // parameters.put(INCLUDE_DRAFTS_KEY, String.valueOf(false)); + + return parameters; + } + + protected void rollbackManagedResources() { + for(CKANResource ckanResource : managedResources) { + try { + ckanResource.rollback(); + } catch(Exception e) { + logger.error("Unable to rollback resource {} to the original value", ckanResource.getResourceID()); + } + + } + } + + protected ArrayNode createResources(ArrayNode resourcesToBeCreated) { + ArrayNode created = mapper.createArrayNode(); + for(JsonNode resourceNode : resourcesToBeCreated) { + CKANResource ckanResource = new CKANResource(itemID); + String json = ckanResource.create(getAsString(resourceNode)); + created.add(getAsJsonNode(json)); + managedResources.add(ckanResource); + } + return created; + } + + protected JsonNode addExtraField(JsonNode jsonNode, String key, String value) { + ArrayNode extras = null; + boolean found = false; + if(jsonNode.has(EXTRAS_KEY)) { + extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); + for(JsonNode extra : extras) { + if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY)!=null && extra.get(EXTRAS_KEY_KEY).asText().compareTo(key) == 0) { + ((ObjectNode) extra).put(EXTRAS_VALUE_KEY, value); + found = true; + break; + } + } + } else { + extras = ((ObjectNode) jsonNode).putArray(EXTRAS_KEY); + } + + if(!found) { + ObjectNode extra = mapper.createObjectNode(); + extra.put(EXTRAS_KEY_KEY, key); + extra.put(EXTRAS_VALUE_KEY, value); + extras.add(extra); + } + + return jsonNode; + } + + protected JsonNode getExtraField(JsonNode jsonNode, String key) { + if(jsonNode.has(EXTRAS_KEY)) { + ArrayNode extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); + for(JsonNode extra : extras) { + if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY)!=null && extra.get(EXTRAS_KEY_KEY).asText().compareTo(key) == 0) { + return extra.get(EXTRAS_VALUE_KEY); + } + } + } + return null; + } + + + protected String addItemURLViaResolver(JsonNode jsonNode) { + // Adding Item URL via Resolver + URIResolver uriResolver = URIResolver.getInstance(); + itemURL = uriResolver.getCatalogueItemURL(name); + addExtraField(jsonNode, EXTRAS_ITEM_URL_KEY, itemURL); + return itemURL; + } + + protected void sendSocialPost(String userFullName) { + try { + boolean makePost = false; + try { + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + if(queryParameters.containsKey(GCatConstants.SOCIAL_POST_QUERY_PARAMETER)) { + makePost = Boolean.parseBoolean(queryParameters.getFirst(GCatConstants.SOCIAL_POST_QUERY_PARAMETER)); + } + } catch(Exception e) { + makePost = false; + } + + if(makePost) { + ArrayNode arrayNode = (ArrayNode) result.get(TAGS_KEY); + SocialPost socialPost = new SocialPost(); + socialPost.setUserFullName(userFullName); + socialPost.setItemID(itemID); + socialPost.setItemURL(itemURL); + socialPost.setItemTitle(itemTitle); + socialPost.setTags(arrayNode); + + Boolean notification = null; + try { + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + if(queryParameters.containsKey(GCatConstants.SOCIAL_POST_NOTIFICATION_QUERY_PARAMETER)) { + notification = Boolean.parseBoolean(queryParameters.getFirst(GCatConstants.SOCIAL_POST_NOTIFICATION_QUERY_PARAMETER)); + } + } catch(Exception e) { + + } + socialPost.setNotification(notification); + socialPost.start(); + } else { + logger.info("The request explicitly disabled the Social Post."); + } + } catch(Exception e) { + logger.warn( + "error dealing with Social Post. The service will not raise the exception belove. Please contact the administrator to let him know about this message.", + e); + } + } + + protected boolean isItemCreator() { + return result.get(AUTHOR_EMAIL_KEY).asText().compareTo(ckanUser.getEMail())==0; + } + + protected void parseResult() { + if(this.itemID == null) { + this.itemID = result.get(ID_KEY).asText(); + } + this.itemTitle = result.get(TITLE_KEY).asText(); + JsonNode node = getExtraField(result, EXTRAS_ITEM_URL_KEY); + if(node!=null) { + this.itemURL = node.asText(); + } + } + + + protected void readItem() throws Exception { + if(this.result == null) { + String ret = super.read(); + this.result = mapper.readTree(ret); + } + parseResult(); + } + + + @Override + public String read() { + try { + readItem(); + checkModerationRead(); + // TODO check keepModerationExtraProperties + // uriInfo.getQueryParameters().get(KEEP); + + return getAsCleanedString(result); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.package_create + @Override + public String create(String json) { + try { + logger.debug("Going to create Item {}", json); + + if(ckanUser.getRole().ordinal() < Role.EDITOR.ordinal()) { + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("Only "); + stringBuffer.append(Role.EDITOR.getPortalRole()); + stringBuffer.append(" and "); + stringBuffer.append(Role.ADMIN.getPortalRole()); + stringBuffer.append(" are entitled to create items. "); + stringBuffer.append("Please contact the VRE Manager to request your grant."); + throw new ForbiddenException(stringBuffer.toString()); + } + + JsonNode jsonNode = validateJson(json); + + setItemToPending(jsonNode); + + ArrayNode resourcesToBeCreated = mapper.createArrayNode(); + if(jsonNode.has(RESOURCES_KEY)) { + resourcesToBeCreated = (ArrayNode) jsonNode.get(RESOURCES_KEY); + ((ObjectNode) jsonNode).remove(RESOURCES_KEY); + } + + String context = configuration.getContext(); + ScopeBean scopeBean = new ScopeBean(context); + + if(scopeBean.is(Type.VRE)) { + addItemURLViaResolver(jsonNode); + } + + super.create(getAsString(jsonNode)); + + parseResult(); + ArrayNode created = createResources(resourcesToBeCreated); + ((ObjectNode) result).replace(RESOURCES_KEY, created); + + postItemCreated(); + + if(!isModerationEnabled()) { + if(scopeBean.is(Type.VRE)) { + // Actions performed after a package has been correctly created on ckan. + String userFullName = CKANUserCache.getCurrrentCKANUser().getNameSurname(); + sendSocialPost(userFullName); + } + } + + return getAsCleanedString(result); + } catch(WebApplicationException e) { + rollbackManagedResources(); + throw e; + } catch(Exception e) { + rollbackManagedResources(); + throw new InternalServerErrorException(e); + } + } + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.package_update + @Override + public String update(String json) { + try { + this.updateOperation = true; + + this.result = null; + readItem(); + + JsonNode jsonNode = validateJson(json); + + jsonNode = checkModerationUpdate(jsonNode); + + Map originalResources = new HashMap<>(); + ArrayNode originalResourcesarrayNode = (ArrayNode) result.get(RESOURCES_KEY); + if(originalResources != null) { + for(JsonNode resourceNode : originalResourcesarrayNode) { + CKANResource ckanResource = new CKANResource(itemID); + ckanResource.setPreviousRepresentation(resourceNode); + String resourceID = ckanResource.getResourceID(); + originalResources.put(resourceID, ckanResource); + } + } + + if(jsonNode.has(RESOURCES_KEY)) { + ArrayNode resourcesToBeSend = mapper.createArrayNode(); + ArrayNode receivedResources = (ArrayNode) jsonNode.get(RESOURCES_KEY); + for(JsonNode resourceNode : receivedResources) { + CKANResource ckanResource = new CKANResource(itemID); + String resourceId = CKANResource.extractResourceID(resourceNode); + if(resourceId != null) { + if(originalResources.containsKey(resourceId)) { + ckanResource = originalResources.get(resourceId); + originalResources.remove(resourceId); + } else { + throw new BadRequestException( + "The content contains a resource with id " + resourceId + " which does not exists"); + } + } + if(originalResources.get(resourceId) != null + && (!originalResources.get(resourceId).getPreviousRepresentation().equals(resourceNode))) { + resourceNode = ckanResource.createOrUpdate(resourceNode); + } + resourcesToBeSend.add(resourceNode); + managedResources.add(ckanResource); + + } + ((ObjectNode) jsonNode).replace(RESOURCES_KEY, resourcesToBeSend); + } + + addItemURLViaResolver(jsonNode); + + sendPostRequest(ITEM_UPDATE, getAsString(jsonNode)); + + for(String resourceId : originalResources.keySet()) { + CKANResource ckanResource = originalResources.get(resourceId); + ckanResource.deleteFile(); + } + + postItemUpdated(); + + return getAsCleanedString(result); + } catch(WebApplicationException e) { + rollbackManagedResources(); + throw e; + } catch(Exception e) { + rollbackManagedResources(); + throw new InternalServerErrorException(e); + } + } + + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.patch.package_patch + @Override + public String patch(String json) { + try { + this.updateOperation = true; + + this.result = null; + readItem(); + + JsonNode jsonNode = checkBaseInformation(json, true); + ((ObjectNode)jsonNode).put(ID_KEY, this.itemID); + + jsonNode = checkModerationUpdate(jsonNode); + + Map originalResources = new HashMap<>(); + ArrayNode originalResourcesarrayNode = (ArrayNode) result.get(RESOURCES_KEY); + if(originalResources != null) { + for(JsonNode resourceNode : originalResourcesarrayNode) { + CKANResource ckanResource = new CKANResource(itemID); + ckanResource.setPreviousRepresentation(resourceNode); + String resourceID = ckanResource.getResourceID(); + originalResources.put(resourceID, ckanResource); + } + } + + if(jsonNode.has(RESOURCES_KEY)) { + ArrayNode resourcesToBeSend = mapper.createArrayNode(); + ArrayNode receivedResources = (ArrayNode) jsonNode.get(RESOURCES_KEY); + for(JsonNode resourceNode : receivedResources) { + CKANResource ckanResource = new CKANResource(itemID); + String resourceId = CKANResource.extractResourceID(resourceNode); + if(resourceId != null) { + if(originalResources.containsKey(resourceId)) { + ckanResource = originalResources.get(resourceId); + originalResources.remove(resourceId); + } else { + throw new BadRequestException( + "The content contains a resource with id " + resourceId + " which does not exists"); + } + } + if(originalResources.get(resourceId) != null + && (!originalResources.get(resourceId).getPreviousRepresentation().equals(resourceNode))) { + resourceNode = ckanResource.createOrUpdate(resourceNode); + } + resourcesToBeSend.add(resourceNode); + managedResources.add(ckanResource); + + } + ((ObjectNode) jsonNode).replace(RESOURCES_KEY, resourcesToBeSend); + } + + if(jsonNode.has(EXTRAS_KEY)) { + addItemURLViaResolver(jsonNode); + } + + sendPostRequest(ITEM_PATCH, getAsString(jsonNode)); + + parseResult(); + + for(String resourceId : originalResources.keySet()) { + CKANResource ckanResource = originalResources.get(resourceId); + ckanResource.deleteFile(); + } + + postItemUpdated(); + + return getAsCleanedString(result); + } catch(WebApplicationException e) { + rollbackManagedResources(); + throw e; + } catch(Exception e) { + rollbackManagedResources(); + throw new InternalServerErrorException(e); + } + } + + @Override + protected void delete() { + checkModerationDelete(); + super.delete(); + postItemDeleted(); + } + + @Override + public void purge() { + try { + setApiKey(CKANUtility.getSysAdminAPI()); + readItem(); + + checkModerationDelete(); + + if(ckanUser.getRole().ordinal() < Role.ADMIN.ordinal() && !isItemCreator()) { + throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to purge an item"); + } + + if(result.has(RESOURCES_KEY)) { + itemID = result.get(ID_KEY).asText(); + ArrayNode arrayNode = (ArrayNode) result.get(RESOURCES_KEY); + for(JsonNode jsonNode : arrayNode) { + CKANResource ckanResource = new CKANResource(itemID); + ckanResource.setPreviousRepresentation(jsonNode); + ckanResource.deleteFile(); // Only delete file is required because the item will be purged at the end + } + } + super.purge(); + + postItemDeleted(); + + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + /** + * Used for bulk purging. Internal use only + */ + public void purgeNoCheckNoDeleteFiles() { + setApiKey(CKANUtility.getSysAdminAPI()); + super.purge(); + } + + /* + * -------------------------------------------------------------------------------------------------------- + * Moderation Related functions + * -------------------------------------------------------------------------------------------------------- + * + */ + protected CMItemStatus getCMItemStatus() { + + String cmItemStatusString = CMItemStatus.APPROVED.getValue(); + boolean found = false; + if(result.has(EXTRAS_KEY)) { + ArrayNode extras = (ArrayNode) result.get(EXTRAS_KEY); + for(JsonNode extra : extras) { + if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY).asText().compareTo(Moderated.SYSTEM_CM_ITEM_STATUS) == 0) { + cmItemStatusString = extra.get(EXTRAS_VALUE_KEY).asText(); + found = true; + break; + } + } + } + + CMItemStatus cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString); + + if(!found) { + // The item was published before activating the moderation. + // The item is considered as approved and the item representation must be updated + setToApproved(result); + String ret = sendPostRequest(ITEM_UPDATE, getAsString(result)); + try { + result = mapper.readTree(ret); + }catch (Exception e) { + new InternalServerErrorException(e); + } + } + + return cmItemStatus; + } + + protected CMItemStatus getRequestedCMItemStatus() { + CMItemStatus cmItemStatus = null; + try { + MultivaluedMap queryParameters = uriInfo.getQueryParameters(); + if(queryParameters.containsKey(Moderated.CM_ITEM_STATUS_QUERY_PARAMETER)) { + String cmItemStatusString = queryParameters.getFirst(Moderated.CM_ITEM_STATUS_QUERY_PARAMETER); + cmItemStatus = CMItemStatus.getCMItemStatusFromValue(cmItemStatusString); + } + }catch (Exception e) { + cmItemStatus = null; + } + return cmItemStatus; + } + + protected void setItemAuthorToModerationThread() { + String itemAuthorCkanUsername = ""; + JsonNode jsonNode = getExtraField(result, Moderated.SYSTEM_CM_ITEM_AUTHOR); + if(jsonNode!=null) { + itemAuthorCkanUsername = jsonNode.asText(); + moderationThread.setItemAuthorCkanUsername(itemAuthorCkanUsername); + } + } + + protected String getItemAuthorFullName() { + String itemAuthorFullName = ""; + JsonNode jsonNode = getExtraField(result, Moderated.SYSTEM_CM_ITEM_AUTHOR_FULLNAME); + if(jsonNode!=null) { + itemAuthorFullName = jsonNode.asText(); + } + return itemAuthorFullName; + } + + protected boolean isModerationEnabled() { + boolean moderationEnabled = configuration.isModerationEnabled(); + if(moderationEnabled && moderationThread==null) { + moderationThread = ModerationThread.getDefaultInstance(); + moderationThread.setCKANUser(ckanUser); + } + return moderationEnabled; + } + + protected Map addModerationStatusFilter(Map parameters){ + if(isModerationEnabled()) { + String q = parameters.get(GCatConstants.Q_KEY); + + CMItemStatus cmItemStatus = getRequestedCMItemStatus(); + + this.apiKey = CKANUtility.getSysAdminAPI(); + + if(!ckanUser.isCatalogueModerator()) { + switch (ckanUser.getRole()) { + case ADMIN: + case MANAGER: + break; + + case EDITOR: + if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) { + q = String.format("%s AND %s:%s", q, AUTHOR_EMAIL_KEY, ckanUser.getEMail()); + parameters.put(GCatConstants.Q_KEY, q); + } + break; + + case MEMBER: + if(cmItemStatus!=null && cmItemStatus!=CMItemStatus.APPROVED) { + throw new ForbiddenException("You are only authorized to list " + CMItemStatus.APPROVED.getValue() + " items"); + } + break; + + default: + break; + } + } + + boolean cmItemStatusWasNull = false; + if(cmItemStatus==null) { + cmItemStatusWasNull = true; + cmItemStatus = CMItemStatus.APPROVED; + } + + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("("); + stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY); + stringBuffer.append(":"); + stringBuffer.append(cmItemStatus.getValue()); + + if(cmItemStatusWasNull) { + stringBuffer.append(" OR (*:* -"); + stringBuffer.append(CM_STATUS_QUERY_FILTER_KEY); + stringBuffer.append(":[* TO *])"); + } + + stringBuffer.append(")"); + q = String.format("%s AND %s", q, stringBuffer.toString()); + parameters.put(GCatConstants.Q_KEY, q); + + + parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true)); + }else{ + if(ckanUser.getRole().ordinal()>=Role.ADMIN.ordinal()) { + parameters.put(INCLUDE_PRIVATE_KEY, String.valueOf(true)); + } + } + + return parameters; + } + + protected void checkModerationRead() { + if(isModerationEnabled()) { + CMItemStatus cmItemStatus = getCMItemStatus(); + if(cmItemStatus == CMItemStatus.APPROVED) { + return; + } + + if(isItemCreator()) { + // The author is entitled to read its own items independently from the status + return; + } + + if(ckanUser.getRole().ordinal() >= Role.ADMIN.ordinal() || ckanUser.isCatalogueModerator()) { + // Catalogue-Admin and Catalogue-Moderator are entitled to read items with any statues + return; + } + + throw new ForbiddenException("You are not entitled to read the item"); + } + } + + protected JsonNode checkModerationUpdate(JsonNode jsonNode) { + if(isModerationEnabled()) { + CMItemStatus cmItemStatus = getCMItemStatus(); + + boolean setToPending = true; + + switch (cmItemStatus) { + case APPROVED: + if(ckanUser.getRole().ordinal() < Role.ADMIN.ordinal() && !isItemCreator()) { + throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to update an " + cmItemStatus.getValue() + " item"); + } + if(ckanUser.getRole().ordinal() >= Role.ADMIN.ordinal()) { + setToApproved(jsonNode); + setToPending = false; + } + break; + + case PENDING: + if(isItemCreator()) { + break; + } + if(ckanUser.isCatalogueModerator()) { + break; + } + throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item"); + + case REJECTED: + if(isItemCreator()) { + break; + } + if(ckanUser.isCatalogueModerator()) { + break; + } + throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item"); + + default: + break; + } + + if(setToPending) { + setItemToPending(jsonNode); + } + + } + + return jsonNode; + } + + protected void checkModerationDelete() { + try { + if(isModerationEnabled()) { + readItem(); + + if(ckanUser.getRole().ordinal() >= Role.ADMIN.ordinal()) { + // Ad Admin can delete any item independently from the status + return; + } + + CMItemStatus cmItemStatus = getCMItemStatus(); + + switch (cmItemStatus) { + case APPROVED: + if(isItemCreator()) { + break; + } + throw new ForbiddenException("Only " + Role.ADMIN.getPortalRole() + "s and item creator are entitled to delete an " + cmItemStatus.getValue() + " item"); + + case REJECTED: + if(isItemCreator()) { + break; + } + if(ckanUser.isCatalogueModerator()) { + break; + } + throw new ForbiddenException("You are not entitled to delete a " + cmItemStatus.getValue() + " item"); + + case PENDING: + if(isItemCreator()) { + break; + } + if(ckanUser.isCatalogueModerator()) { + break; + } + throw new ForbiddenException("You are not entitled to update a " + cmItemStatus.getValue() + " item"); + + default: + break; + } + } + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected void setToRejected(JsonNode jsonNode) { + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_STATUS, CMItemStatus.REJECTED.getValue()); + + // Enforcing private again + ((ObjectNode) jsonNode).put(PRIVATE_KEY, true); + /* + * ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, false); + * This code is not properly managed by CKAN. + * It is converted to: + * searchable: "false" + * which is considered as true value. + * We need to provide a string with F as capitol letters to make it working + */ + ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, "False"); + } + + protected void setItemToPending(JsonNode jsonNode) { + if(isModerationEnabled()) { + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_STATUS, CMItemStatus.PENDING.getValue()); + + CMItemVisibility cmItemVisibility = CMItemVisibility.PUBLIC; + + if(jsonNode.has(PRIVATE_KEY)) { + boolean privatePackage = jsonNode.get(PRIVATE_KEY).asBoolean(); + if(privatePackage) { + cmItemVisibility = CMItemVisibility.RESTRICTED; + } + } + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue()); + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_AUTHOR, ckanUser.getName()); + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_AUTHOR_FULLNAME, ckanUser.getNameSurname()); + + ((ObjectNode) jsonNode).put(PRIVATE_KEY, true); + /* + * ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, false); + * This version is not properly managed by CKAN. + * It is converted to: + * searchable: "false" + * which is considered as true value. + * We need to provide a string with F as capitol letters to make it working + */ + ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, "False"); + } + } + + protected void setToApproved(JsonNode jsonNode) { + ArrayNode extras = (ArrayNode) jsonNode.get(EXTRAS_KEY); + + boolean approvedSet = false; + CMItemVisibility cmItemVisibility = null; + + for(JsonNode extra : extras) { + if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY)!=null && extra.get(EXTRAS_KEY_KEY).asText().compareTo(Moderated.SYSTEM_CM_ITEM_STATUS) == 0) { + ((ObjectNode) extra).put(EXTRAS_VALUE_KEY, CMItemStatus.APPROVED.getValue()); + approvedSet = true; + } + + if(extra.has(EXTRAS_KEY_KEY) && extra.get(EXTRAS_KEY_KEY)!=null && extra.get(EXTRAS_KEY_KEY).asText().compareTo(Moderated.SYSTEM_CM_ITEM_VISIBILITY) == 0) { + cmItemVisibility = CMItemVisibility.getCMItemStatusFromValue(extra.get(EXTRAS_VALUE_KEY).asText()); + } + } + + if(!approvedSet) { + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_STATUS, CMItemStatus.APPROVED.getValue()); + } + + if(cmItemVisibility==null) { + cmItemVisibility = CMItemVisibility.PUBLIC; + addExtraField(jsonNode, Moderated.SYSTEM_CM_ITEM_VISIBILITY, cmItemVisibility.getValue()); + } + + + boolean privateItem = cmItemVisibility == CMItemVisibility.RESTRICTED ? true : false; + ((ObjectNode) jsonNode).put(PRIVATE_KEY, privateItem); + if(privateItem) { + ((ObjectNode) jsonNode).put(SEARCHABLE_KEY, "True"); + }else { + ((ObjectNode) jsonNode).remove(SEARCHABLE_KEY); + } + } + + private void postItemCreated() throws Exception { + try { + if(isModerationEnabled()) { + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postItemCreated(); + } + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + private void postItemUpdated() { + try { + if(isModerationEnabled()) { + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postItemUpdated(); + } + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + protected void postItemDeleted() { + try { + if(isModerationEnabled()) { + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postItemDeleted(); + } + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + @Override + public String approve(String moderatorMessage) { + try { + if(isModerationEnabled()) { + readItem(); + CMItemStatus cmItemStatus = getCMItemStatus(); + switch (cmItemStatus) { + case APPROVED: + // Nothing TO DO + break; + + case REJECTED: + throw new BadRequestException("You can't approve a rejected item. The item must be updated first. The update will set the item in pending, than it can be approved/rejected."); + + case PENDING: + if(!ckanUser.isCatalogueModerator()) { + throw new NotAuthorizedException("Only catalogue moderator can approve a pending item."); + } + setToApproved(result); + + // Need to use sysadmin because the user could not have the right to modify the item + setApiKey(CKANUtility.getSysAdminAPI()); + String ret = sendPostRequest(ITEM_UPDATE, getAsString(result)); + // Resetting the api key + setApiKey(null); + + result = mapper.readTree(ret); + + parseResult(); + setItemAuthorToModerationThread(); + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postItemApproved(moderatorMessage); + + String context = configuration.getContext(); + ScopeBean scopeBean = new ScopeBean(context); + + if(scopeBean.is(Type.VRE)) { + // Actions performed after a package has been correctly created on ckan. + String authorFullName = getItemAuthorFullName(); + sendSocialPost(authorFullName); + } + + break; + + default: + break; + } + return getAsCleanedString(result); + } + throw new BadRequestException("The approve operation is available only in moderation mode"); + }catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + @Override + public String reject(String moderatorMessage) { + try { + if(isModerationEnabled()) { + readItem(); + CMItemStatus cmItemStatus = getCMItemStatus(); + switch (cmItemStatus) { + case APPROVED: + throw new BadRequestException("You can't rejected an approved item. The item must be updated first. The update will set the item in pending, than it can be approved/rejected."); + + case REJECTED: + // Nothing TO DO + break; + + case PENDING: + if(!ckanUser.isCatalogueModerator()) { + throw new NotAuthorizedException("Only catalogue moderator can reject a pending item."); + } + + setToRejected(result); + + // Need to use sysadmin because the user could not have the right to modify the item + setApiKey(CKANUtility.getSysAdminAPI()); + String ret = sendPostRequest(ITEM_UPDATE, getAsString(result)); + // Resetting the api key + setApiKey(null); + + result = mapper.readTree(ret); + parseResult(); + setItemAuthorToModerationThread(); + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postItemRejected(moderatorMessage); + break; + + default: + break; + } + return getAsCleanedString(result); + } + throw new BadRequestException("The reject operation is available only in moderation mode"); + }catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + @Override + public void message(String message) { + try { + if(isModerationEnabled()) { + if(message==null || message.compareTo("")==0) { + return; + } + + readItem(); + + isModerationEnabled(); + + // Catalogue Moderators are allowed to post message to the dedicated Stream + if(!ckanUser.isCatalogueModerator()) { + // Users that are not + if(!isItemCreator()) { + throw new NotAuthorizedException("Only item creator and " + Moderated.CATALOGUE_MODERATOR + "s are entitled to partecipate to the moderation discussion thread."); + }else { + moderationThread.setItemAuthor(true); + } + } + + CMItemStatus cmItemStatus = getCMItemStatus(); + setItemAuthorToModerationThread(); + moderationThread.setItemCoordinates(itemID, name, itemTitle, itemURL); + moderationThread.postUserMessage(cmItemStatus, message); + return; + } + throw new BadRequestException("The message operation is available only in moderation mode"); + }catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + public String moderate(String json) { + try { + ModerationContent moderationContent = mapper.readValue(json, ModerationContent.class); + String message = moderationContent.getMessage(); + if(moderationContent.getCMItemStatus() !=null) { + CMItemStatus cmItemStatus = moderationContent.getCMItemStatus(); + switch (cmItemStatus) { + case APPROVED: + return approve(message); + case REJECTED: + return reject(message); + default: + throw new BadRequestException("Allowed moderation operations are approve, reject and message"); + } + }else { + if(message==null || message.length()==0) { + throw new BadRequestException("Allowed moderation operations are approve, reject and message"); + } + message(message); + return null; + } + }catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackageTrash.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackageTrash.java new file mode 100644 index 0000000..0c26fd2 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANPackageTrash.java @@ -0,0 +1,223 @@ +package org.gcube.gcat.persistence.ckan; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.WebApplicationException; + +import org.gcube.com.fasterxml.jackson.databind.ObjectMapper; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.gcat.api.configuration.CKANDB; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.api.roles.Role; +import org.gcube.gcat.configuration.CatalogueConfigurationFactory; +import org.gcube.gcat.persistence.ckan.cache.CKANUserCache; +import org.postgresql.core.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANPackageTrash { + + protected static final Logger logger = LoggerFactory.getLogger(CKANPackageTrash.class); + + private static final String GROUP_TABLE_KEY = "group"; + private static final String GROUP_ID_KEY = "id"; + private static final String GROUP_NAME_KEY = "name"; + + private static final String PACKAGE_TABLE_KEY = "package"; + + private static final String PACKAGE_NAME_KEY = "name"; + + private static final String PACKAGE_TYPE_KEY = "type"; + private static final String PACKAGE_TYPE_VALUE = "dataset"; + + private static final String PACKAGE_STATE_KEY = "state"; + private static final String PACKAGE_STATE_VALUE = "deleted"; + + private static final String PACKAGE_OWNER_ORG_KEY = "owner_org"; + + protected ObjectMapper mapper; + + protected final CKANUser ckanUser; + protected final CatalogueConfiguration configuration; + protected final Set supportedOrganizations; + + protected boolean ownOnly; + + public CKANPackageTrash() { + this(CatalogueConfigurationFactory.getInstance()); + } + + protected CKANPackageTrash(CatalogueConfiguration configuration) { + this.mapper = new ObjectMapper(); + this.ckanUser = CKANUserCache.getCurrrentCKANUser(); + this.configuration = configuration; + this.supportedOrganizations = configuration.getSupportedOrganizations(); + this.ownOnly = true; + } + + public void setOwnOnly(boolean ownOnly) { + this.ownOnly = ownOnly; + } + + protected Connection getConnection() throws Exception { + Class.forName("org.postgresql.Driver"); + CKANDB ckanDB = configuration.getCkanDB(); + String url = ckanDB.getUrl(); + String username = ckanDB.getUsername(); + String password = ckanDB.getPassword(); + Connection connection = DriverManager.getConnection(url, username, password); + logger.trace("Database {} opened successfully", url); + connection.setAutoCommit(false); + return connection; + } + + protected String getQuotedString(String string) throws SQLException { + StringBuilder builder = new StringBuilder(); + builder.append("'"); + Utils.escapeLiteral(builder, string, false); + builder.append("'"); + return builder.toString(); + } + + protected ArrayNode getItems() throws WebApplicationException { + Connection connection = null; + try { + StringBuffer stringBufferOrg = new StringBuffer(); + stringBufferOrg.append("SELECT "); + stringBufferOrg.append(GROUP_ID_KEY); + stringBufferOrg.append(" FROM \""); + stringBufferOrg.append(GROUP_TABLE_KEY); + stringBufferOrg.append("\" WHERE "); + stringBufferOrg.append(GROUP_NAME_KEY); + stringBufferOrg.append(" IN "); + stringBufferOrg.append("("); + boolean first = true; + for(String organizationName : supportedOrganizations) { + if(first) { + first = false; + }else { + stringBufferOrg.append(","); + } + stringBufferOrg.append(getQuotedString(organizationName)); + } + stringBufferOrg.append(")"); + + + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("SELECT "); + stringBuffer.append(PACKAGE_NAME_KEY); + stringBuffer.append(" FROM "); + stringBuffer.append(PACKAGE_TABLE_KEY); + stringBuffer.append(" WHERE "); + stringBuffer.append(PACKAGE_TYPE_KEY); + stringBuffer.append("="); + stringBuffer.append(getQuotedString(PACKAGE_TYPE_VALUE)); + stringBuffer.append(" AND "); + stringBuffer.append(PACKAGE_STATE_KEY); + stringBuffer.append("="); + stringBuffer.append(getQuotedString(PACKAGE_STATE_VALUE)); + + if(ownOnly || ckanUser.getRole().ordinal() extensions = mimeTypeClzInstance.getExtensions(); + if(format == null || format.compareTo("") == 0) { + format = mimeType.split("/")[1].split(";")[0]; + } + } catch(Exception e) { + try { + format = mimeType.split("/")[1].split(";")[0]; + } catch(Exception ex) { + format = null; + } + } + } + if(format != null && format.startsWith(".")) { + format = format.substring(1); + } + return format; + } + + protected ObjectNode persistStorageFile(ObjectNode objectNode) { + + if(objectNode.has(URL_KEY)) { + String urlString = objectNode.get(URL_KEY).asText(); + + URL url; + try { + url = new URL(urlString); + } catch(MalformedURLException e) { + throw new BadRequestException(e); + } + + url = copyStorageResource(url); + + if(name != null) { + objectNode.put(NAME_KEY, name); + } + + if(mimeType != null) { + objectNode.put(MIME_TYPE_KEY, mimeType); + + if(!objectNode.has(FORMAT_KEY)) { + String format = getFormat(); + if(format != null) { + objectNode.put(FORMAT_KEY, format); + } + } + } + + objectNode.put(URL_KEY, url.toString()); + return objectNode; + } + + String error = String.format("The content must contains the %s property", URL_KEY); + throw new BadRequestException(error); + + } + + protected ObjectNode validate(String json) throws MalformedURLException { + JsonNode jsonNode = getAsJsonNode(json); + return validate(jsonNode); + } + + protected ObjectNode validate(JsonNode jsonNode) { + + ObjectNode objectNode = (ObjectNode) jsonNode; + + if(objectNode.has(PACKAGE_ID_KEY)) { + String packageId = objectNode.get(PACKAGE_ID_KEY).asText(); + if(packageId.compareTo(itemID) != 0) { + String error = String.format( + "Item ID %s does not match %s which is the value of %s contained in the representation.", + itemID, packageId, PACKAGE_ID_KEY); + throw new BadRequestException(error); + } + } else { + objectNode.put(PACKAGE_ID_KEY, itemID); + } + + if(objectNode.has(ID_KEY)) { + String gotId = objectNode.get(ID_KEY).asText(); + if(resourceID == null) { + resourceID = gotId; + } else { + if(resourceID.compareTo(gotId) != 0) { + String error = String.format( + "Resource ID %s does not match %s which is the value of %s contained in the representation.", + resourceID, gotId, ID_KEY); + throw new BadRequestException(error); + } + } + } else { + resourceID = TEMP + UUID.randomUUID().toString(); + logger.trace( + "The id of the resource with name {} for package {} has not been provided. It has been generated : {}", + name, itemID, resourceID); + } + + return objectNode; + } + + protected URL getFinalURL(String url) { + try { + URL urlURL = new URL(url); + return CKANResource.getFinalURL(urlURL); + } catch(MalformedURLException e) { + throw new BadRequestException(e); + } + } + + public static URL getFinalURL(URL url) { + HTTPCall httpCall = new HTTPCall(url.toString()); + httpCall.setgCubeTargetService(false); + URL finalURL = httpCall.getFinalURL(url); + return finalURL; + } + + protected boolean isStorageFile(URL url) { + URL urlToCheck = url; + try { + urlToCheck = getFinalURL(url); + } catch(Exception e) { + // TODO Evaluate if we want to validate the URL. If the URL does not exists the service + // could decide to refuse the Resource Creation + } + if(urlToCheck.getHost().compareTo(URI_RESOLVER_STORAGE_HUB_HOST) == 0) { + if(urlToCheck.getPath().startsWith(URI_RESOLVER_STORAGE_HUB_PATH)) { + persistedURL = urlToCheck; + return true; + } + } + return false; + } + + /** + * Check if the URl is a workspace URL so that is has to copy the resource to guarantee + * the resource remain persistent + * @param url the URL to check + * @return the public URL of the copied resource if any. It return the original URL otherwise + */ + protected URL copyStorageResource(URL url) { + persistedURL = url; + if(isStorageFile(persistedURL)) { + try { + persistedURL = storageHubManagement.ensureResourcePersistence(persistedURL, itemID, resourceID); + String originalFilename = storageHubManagement.getOriginalFilename(); + name = FilenameUtils.removeExtension(originalFilename); + originalFileExtension = FilenameUtils.getExtension(originalFilename); + mimeType = storageHubManagement.getMimeType(); + persisted = true; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + return persistedURL; + } + + protected void deleteStorageResource(URL url, String filename, String mimetype) { + persistedURL = url; + if(isStorageFile(persistedURL)) { + try { + GXHTTPStringRequest gxhttpStringRequest = GXHTTPStringRequest.newRequest(persistedURL.toString()); + HttpURLConnection httpURLConnection = gxhttpStringRequest.from(Constants.CATALOGUE_NAME).head(); + String storageHubContentType = httpURLConnection.getContentType().split(";")[0]; + if(mimetype.compareTo(storageHubContentType) != 0) { + mimetype = storageHubContentType; + // Using storage hub mimetype + } + } catch(Exception e) { + // using provided mimetype + } + try { + storageHubManagement.deleteResourcePersistence(itemID, filename, mimetype); + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + } + + protected String create(JsonNode jsonNode) { + try { + ObjectNode objectNode = validate(jsonNode); + objectNode = persistStorageFile(objectNode); + String ret = super.create(getAsString(objectNode)); + if(persisted) { + String gotResourceID = result.get(ID_KEY).asText(); + if(gotResourceID != null && gotResourceID.compareTo(resourceID) != 0) { + resourceID = gotResourceID; +// String revisionID = result.get(REVISION_ID_KEY).asText(); +// storageHubManagement.renameFile(resourceID, revisionID); + storageHubManagement.renameFile(resourceID); + } + } + return ret; + } catch(WebApplicationException e) { + // TODO Remove created file if any + throw e; + } catch(Exception e) { + // TODO Remove created file if any + throw new InternalServerErrorException(e); + } + } + + @Override + public String create(String json) { + JsonNode jsonNode = getAsJsonNode(json); + return create(jsonNode); + } + + @Override + public String read() { + return sendGetRequest(READ, getMapWithID(resourceID)); + } + + protected String update(JsonNode jsonNode) throws Exception { + ObjectNode resourceNode = (ObjectNode) jsonNode; + // This cannot be moved outside otherwise we don't + resourceNode = validate(resourceNode); + + getPreviousRepresentation(); + + String oldURL = previousRepresentation.get(CKANResource.URL_KEY).asText(); + String newURL = resourceNode.get(CKANResource.URL_KEY).asText(); + + if(!previousRepresentation.equals(resourceNode)) { + if(oldURL.compareTo(newURL) != 0) { + logger.trace("The URL of the resource with id {} was not changed", resourceID); + this.mimeType = previousRepresentation.get(CKANResource.MIME_TYPE_KEY).asText(); + + try { + storageHubManagement.retrievePersistedFile(resourceID, mimeType); + }catch (Exception e) { + // If the file was not persisted by gCat (e.g. created with the portlet) some errors can occurs + } + + } else { + logger.trace("The URL of resource with id {} has been changed the old URL was {}, the new URL is {}", + resourceID, oldURL, newURL); + resourceNode = persistStorageFile(resourceNode); + /* + try { + URL urlOLD = new URL(oldURL); + deleteStorageResource(urlOLD); + }catch (Exception e) { + logger.error("Unable to remove old file at URL {}", oldURL); + } + */ + } + String ret = super.update(getAsString(resourceNode)); +// if(storageHubManagement.getPersistedFile()!= null) { +// String revisionID = result.get(REVISION_ID_KEY).asText(); +// storageHubManagement.addRevisionID(resourceID, revisionID); +// } + return ret; + } + + return previousRepresentation.asText(); + } + + @Override + public String update(String json) { + try { + JsonNode jsonNode = getAsJsonNode(json); + return update(jsonNode); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new WebApplicationException(e); + } + } + + @Override + public String patch(String json) { + String[] moreAllowed = new String[] {HEAD.class.getSimpleName(), GET.class.getSimpleName(), + PUT.class.getSimpleName(), DELETE.class.getSimpleName()}; + throw new NotAllowedException(OPTIONS.class.getSimpleName(), moreAllowed); + } + + @Override + public void delete(boolean purge) { + delete(); + } + + @Override + public void delete() { + try { + deleteFile(); + sendPostRequest(DELETE, createJsonNodeWithID(resourceID)); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new WebApplicationException(e); + } + } + + @Override + public void purge() { + String[] moreAllowed = new String[] {HEAD.class.getSimpleName(), GET.class.getSimpleName(), + PUT.class.getSimpleName(), DELETE.class.getSimpleName()}; + throw new NotAllowedException(OPTIONS.class.getSimpleName(), moreAllowed); + } + + public JsonNode createOrUpdate(JsonNode jsonNode) { + ObjectNode resourceNode = (ObjectNode) jsonNode; + if(resourceNode.has(ID_KEY)) { + try { + update(resourceNode); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new WebApplicationException(e); + } + } else { + create(resourceNode); + } + return result; + } + + public void deleteFile() { + try { + getPreviousRepresentation(); + URL url = new URL(previousRepresentation.get(URL_KEY).asText()); + mimeType = previousRepresentation.get(MIME_TYPE_KEY).asText(); + deleteStorageResource(url, resourceID, mimeType); + } catch(Exception e) { + logger.error("Unable to delete resource {}", + previousRepresentation != null ? getAsString(previousRepresentation) : ""); + } + } + + public void rollback() { + if(previousRepresentation != null) { + try { + update(previousRepresentation); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new WebApplicationException(e); + } + } else { + delete(); + } + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANUser.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANUser.java new file mode 100644 index 0000000..509ba39 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANUser.java @@ -0,0 +1,339 @@ +package org.gcube.gcat.persistence.ckan; + +import java.util.Collection; + +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.com.fasterxml.jackson.databind.node.ObjectNode; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.user.User; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.api.moderation.Moderated; +import org.gcube.gcat.api.roles.Role; +import org.gcube.gcat.configuration.CatalogueConfigurationFactory; +import org.gcube.gcat.utils.RandomString; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANUser extends CKAN { + + private static final Logger logger = LoggerFactory.getLogger(CKANUser.class); + + /* User Paths */ + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.user_list + public static final String USER_LIST = CKAN.CKAN_API_PATH + "user_list"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.create.user_create + public static final String USER_CREATE = CKAN.CKAN_API_PATH + "user_create"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.get.user_show + public static final String USER_SHOW = CKAN.CKAN_API_PATH + "user_show"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.update.user_update + public static final String USER_UPDATE = CKAN.CKAN_API_PATH + "user_update"; + // see https://docs.ckan.org/en/latest/api/#ckan.logic.action.delete.user_delete + public static final String USER_DELETE = CKAN.CKAN_API_PATH + "user_delete"; + + public static final String ADD_USER_TO_GROUP = CKAN.CKAN_API_PATH + "member_create"; + + public static final String NAME = "name"; + public static final String DISPLAY_NAME = "display_name"; + public static final String FULL_NAME = "fullname"; + public static final String ABOUT = "about"; + public static final String EMAIL = "email"; + public static final String PASSWORD = "password"; + + private static final String API_KEY = "apikey"; + + public static final String PORTAL_ROLES = "portal_roles"; + + protected String nameSurname; + protected Role role; + protected Boolean catalogueModerator; + + protected CatalogueConfiguration configuration; + + public CKANUser() { + this(CatalogueConfigurationFactory.getInstance()); + } + + protected CKANUser(CatalogueConfiguration configuration) { + super(); + this.LIST = USER_LIST; + this.CREATE = USER_CREATE; + this.READ = USER_SHOW; + this.UPDATE = USER_UPDATE; + this.PATCH = null; + this.DELETE = USER_DELETE; + this.PURGE = null; + this.catalogueModerator = null; + this.configuration = configuration; + } + + public void setName(String name) { + name = getCKANUsername(name); + this.name = name; + } + + public String createInCkan() { + RandomString randomString = new RandomString(12); + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put(NAME, name); + objectNode.put(PASSWORD, randomString.nextString()); + checkAndSetEmail(objectNode); + checkAndSetFullName(objectNode); + checkAndSetJobTitle(objectNode); + return create(getAsString(objectNode)); + } + + @Override + public void delete(boolean purge) { + this.delete(); + } + + /** + * + * @param objectNode + * @return true if the display name and the full name has been updated in objectNode + */ + private boolean checkAndSetJobTitle(ObjectNode objectNode) { + String jobTitle = SecretManagerProvider.instance.get().getUser().getAbout(); + + String ckanJobTitle = ""; + if(objectNode.has(ABOUT)) { + ckanJobTitle = objectNode.get(ABOUT).asText(); + } + if(jobTitle!=null && jobTitle.compareTo(ckanJobTitle) != 0) { + objectNode.put(ABOUT, jobTitle); + return true; + } + return false; + } + + /** + * + * @param objectNode + * @return true if the display name and the full name has been updated in objectNode + */ + private boolean checkAndSetFullName(ObjectNode objectNode) { + User user = SecretManagerProvider.instance.get().getUser(); + String portalFullname = user.getFullName(); + this.nameSurname = user.getFullName(true); + + String ckanFullname = ""; + if(objectNode.has(FULL_NAME)) { + ckanFullname = objectNode.get(FULL_NAME).asText(); + } + if(portalFullname!=null && portalFullname.compareTo(ckanFullname) != 0) { + objectNode.put(FULL_NAME, portalFullname); + objectNode.put(DISPLAY_NAME, portalFullname); + return true; + } + return false; + } + + /** + * + * @param objectNode + * @return true if the display name and the full name has been updated + */ + private boolean checkAndSetEmail(ObjectNode objectNode) { + User user = SecretManagerProvider.instance.get().getUser(); + String portalEmail = user.getEmail(); + + String ckanEmail = ""; + if(objectNode.has(EMAIL)) { + ckanEmail = objectNode.get(EMAIL).asText(); + } + if(portalEmail==null) { + String username = user.getUsername(); + String eMail = username + "@d4science.org"; + objectNode.put(EMAIL, eMail); + return true; + } else if(portalEmail.compareTo(ckanEmail) != 0) { + objectNode.put(EMAIL, portalEmail); + return true; + } + return false; + } + + /** + * Update the user profile on CKAN if the got got informations differs from the portal information + * @return true if the profile information has been updated + */ + protected boolean updateProfileIfNeeded() { + ObjectNode objectNode = (ObjectNode) result; + boolean toBeUpdated = false; + + toBeUpdated = checkAndSetEmail(objectNode) || toBeUpdated; + toBeUpdated = checkAndSetFullName(objectNode) || toBeUpdated; + toBeUpdated = checkAndSetJobTitle(objectNode) || toBeUpdated; + + if(toBeUpdated) { + update(getAsString(objectNode)); + } + return toBeUpdated; + } + + public void retrieve() { + setApiKey(CKANUtility.getSysAdminAPI()); + try { + if(name == null || name.compareTo("") == 0) { + setName(getCKANUsername()); + } + read(); + updateProfileIfNeeded(); + } catch(WebApplicationException e) { + if(e.getResponse().getStatusInfo() == Status.NOT_FOUND) { + createInCkan(); + } else { + throw e; + } + } + try { + CatalogueConfiguration configuration = CatalogueConfigurationFactory.getInstance(); + for(String supportedOrganization : configuration.getSupportedOrganizations()) { + addUserToOrganization(supportedOrganization); + } + }catch (Exception e) { + // The organization could not exists and this is fine in some cases like organization create or + // for listing items at VO level. The organization corresponding to the VO could not exists. + logger.warn("Add user to organization {} failed. This is acceptable in the case the request is at VO level and the corresponding orgnization does not esists and should not, as well as when the organization is going to be created", CKANOrganization.getCKANOrganizationName()); + } + } + + protected String parseResult() { + name = result.get(NAME).asText(); + + // Only managers can read Ckan API key + if(getRole().ordinal() roles = SecretManagerProvider.instance.get().getUser().getRoles(); + for(String portalRole : roles) { + Role gotRole = Role.getRoleFromPortalRole(portalRole); + if(gotRole != null && gotRole.ordinal() > role.ordinal()) { + role = gotRole; + } + } + } + return role; + } + + public void addUserToOrganization(String organizationName) { + addUserToOrganization(organizationName, name, getRole().getCkanRole()); + } + + public void addUserToOrganization() { + String organizationName = CKANOrganization.getCKANOrganizationName(); + addUserToOrganization(organizationName); + } + + public void addToGroup(String groupName) throws WebApplicationException { + try { + ObjectNode objectNode = mapper.createObjectNode(); + objectNode.put(ID_KEY, CKANGroup.getCKANGroupName(groupName)); + objectNode.put("object", name); + objectNode.put("object_type", "user"); + objectNode.put("capacity", "member"); + sendPostRequest(ADD_USER_TO_GROUP, getAsString(objectNode)); + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + public boolean isCatalogueModerator() { + if(catalogueModerator == null) { + catalogueModerator = SecretManagerProvider.instance.get().getUser().getRoles().contains(Moderated.CATALOGUE_MODERATOR); + } + return catalogueModerator; + } + + public String getSurnameName(){ + return result.get(FULL_NAME).asText(); + } + + public String getNameSurname() { + return nameSurname; + } + + public String getEMail() { + return result.get(EMAIL).asText(); + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/CKANUtility.java b/src/main/java/org/gcube/gcat/persistence/ckan/CKANUtility.java new file mode 100644 index 0000000..8eb5320 --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/CKANUtility.java @@ -0,0 +1,38 @@ +package org.gcube.gcat.persistence.ckan; + +import javax.ws.rs.InternalServerErrorException; + +import org.gcube.gcat.configuration.CatalogueConfigurationFactory; +import org.gcube.gcat.persistence.ckan.cache.CKANUserCache; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class CKANUtility { + + public static String getCkanURL() { + try { + return CatalogueConfigurationFactory.getInstance().getCkanURL(); + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + public static String getSysAdminAPI() { + try { + return CatalogueConfigurationFactory.getInstance().getSysAdminToken(); + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + + public static String getApiKey() { + try { + CKANUser ckanUser = CKANUserCache.getCurrrentCKANUser(); + return ckanUser.getApiKey(); + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + } + +} diff --git a/src/main/java/org/gcube/gcat/persistence/ckan/cache/CKANUserCache.java b/src/main/java/org/gcube/gcat/persistence/ckan/cache/CKANUserCache.java new file mode 100644 index 0000000..cd5140b --- /dev/null +++ b/src/main/java/org/gcube/gcat/persistence/ckan/cache/CKANUserCache.java @@ -0,0 +1,84 @@ +package org.gcube.gcat.persistence.ckan.cache; + +import java.util.concurrent.TimeUnit; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.expiry.CreatedExpiryPolicy; +import javax.cache.expiry.Duration; +import javax.cache.spi.CachingProvider; + +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.gcat.persistence.ckan.CKANUser; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public abstract class CKANUserCache { + + private static final CacheManager cacheManager; + + private static final MutableConfiguration userCacheConfiguration; + + static { + CachingProvider provider = Caching.getCachingProvider(); + cacheManager = provider.getCacheManager(); + + userCacheConfiguration = new MutableConfiguration().setTypes(String.class, CKANUser.class) + .setStoreByValue(false) + .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.MINUTES, 15))); + } + + private CKANUserCache() { + } + + public synchronized static CKANUser getCurrrentCKANUser() { + SecretManager secretManager = SecretManagerProvider.instance.get(); + String gcubeUsername = secretManager.getUser().getUsername(); + String context = secretManager.getContext(); + Cache userCache = cacheManager.getCache(context); + if(userCache == null) { + userCache = cacheManager.createCache(context, userCacheConfiguration); + } + + CKANUser ckanUser = userCache.get(gcubeUsername); + if(ckanUser == null) { + ckanUser = new CKANUser(); + ckanUser.retrieve(); + userCache.put(gcubeUsername, ckanUser); + } + return ckanUser; + } + + + public synchronized static void removeUserFromCache() { + SecretManager secretManager = SecretManagerProvider.instance.get(); + String gcubeUsername = secretManager.getUser().getUsername(); + removeUserFromCache(gcubeUsername); + } + + public synchronized static void removeUserFromCache(String gcubeUsername) { + SecretManager secretManager = SecretManagerProvider.instance.get(); + String context = secretManager.getContext(); + Cache userCache = cacheManager.getCache(context); + if(userCache != null) { + userCache.remove(gcubeUsername); + } + } + + public synchronized static void emptyUserCache() { + SecretManager secretManager = SecretManagerProvider.instance.get(); + String context = secretManager.getContext(); + cacheManager.destroyCache(context); + } + + @Override + protected void finalize() throws Throwable { + super.finalize(); + cacheManager.close(); + } + +} diff --git a/src/main/java/org/gcube/gcat/profile/MetadataUtility.java b/src/main/java/org/gcube/gcat/profile/MetadataUtility.java new file mode 100644 index 0000000..8de907e --- /dev/null +++ b/src/main/java/org/gcube/gcat/profile/MetadataUtility.java @@ -0,0 +1,70 @@ +package org.gcube.gcat.profile; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; + +import org.gcube.datacatalogue.metadatadiscovery.DataCalogueMetadataFormatReader; +import org.gcube.datacatalogue.metadatadiscovery.bean.MetadataProfile; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.MetadataFormat; +import org.gcube.datacatalogue.metadatadiscovery.bean.jaxb.NamespaceCategory; +import org.xml.sax.SAXException; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class MetadataUtility { + + private DataCalogueMetadataFormatReader dataCalogueMetadataFormatReader; + + /* + * this map contains the Metadata Profiles. The key is the name of the profile. + */ + private Map metadataProfiles; + + public MetadataUtility() throws Exception { + dataCalogueMetadataFormatReader = new DataCalogueMetadataFormatReader(); + } + + public void validateProfile(String xmlProfile) throws ParserConfigurationException, SAXException, IOException { + dataCalogueMetadataFormatReader.validateProfile(xmlProfile); + } + + public Map getMetadataProfiles() throws Exception { + if(metadataProfiles == null) { + metadataProfiles = new HashMap<>(); + List list = dataCalogueMetadataFormatReader.getListOfMetadataProfiles(); + for(MetadataProfile profile : list) { + metadataProfiles.put(profile.getName(), profile); + } + } + return metadataProfiles; + } + + /** + * Returns the names of the metadata profiles in a given context + * @return the set of profile names + * @throws Exception + */ + public Set getProfilesNames() throws Exception { + return getMetadataProfiles().keySet(); + } + + public MetadataFormat getMetadataFormat(String profileName) throws Exception { + MetadataProfile profile = getMetadataProfiles().get(profileName); + if(profile != null) { + return dataCalogueMetadataFormatReader.getMetadataFormatForMetadataProfile(profile); + } + return null; + } + + public List getNamespaceCategories() throws Exception { + return dataCalogueMetadataFormatReader.getListOfNamespaceCategories(); + + } + +} diff --git a/src/main/java/org/gcube/gcat/social/SocialPost.java b/src/main/java/org/gcube/gcat/social/SocialPost.java new file mode 100644 index 0000000..c503e10 --- /dev/null +++ b/src/main/java/org/gcube/gcat/social/SocialPost.java @@ -0,0 +1,175 @@ +package org.gcube.gcat.social; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; + +import org.gcube.com.fasterxml.jackson.databind.JsonNode; +import org.gcube.com.fasterxml.jackson.databind.node.ArrayNode; +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.gcat.api.configuration.CatalogueConfiguration; +import org.gcube.gcat.configuration.CatalogueConfigurationFactory; +import org.gcube.gcat.utils.Constants; +import org.gcube.portal.databook.shared.Post; +import org.gcube.social_networking.social_networking_client_library.PostClient; +import org.gcube.social_networking.socialnetworking.model.beans.PostInputBean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SocialPost extends Thread { + + private static final Logger logger = LoggerFactory.getLogger(SocialPost.class); + + protected static final String NOTIFICATION_MESSAGE = "%s just published the item \"%s\"\n" + + "Please find it at %s\n"; + + protected String userFullName; + protected String itemID; + protected String itemURL; + protected String itemTitle; + protected List tags; + protected Boolean notification; + + public SocialPost() throws Exception { + super(); + } + + public String getUserFullName() { + return userFullName; + } + + public void setUserFullName(String userFullName) { + this.userFullName = userFullName; + } + + public String getItemID() { + return itemID; + } + + public void setItemID(String itemID) { + this.itemID = itemID; + } + + public String getItemURL() { + return itemURL; + } + + public void setItemURL(String itemURL) { + this.itemURL = itemURL; + } + + public String getItemTitle() { + return itemTitle; + } + + public void setItemTitle(String itemTitle) { + this.itemTitle = itemTitle; + } + + public List getTags() { + return tags; + } + + public void setTags(List tags) { + this.tags = tags; + } + + public void setTags(ArrayNode tags) { + this.tags = new ArrayList<>(); + if(tags != null && tags.size() > 0) { + for(int i = 0; i < tags.size(); i++) { + JsonNode jsonNode = tags.get(i); + String tagName = ""; + if(jsonNode.has("display_name")) { + tagName = jsonNode.get("display_name").asText(); + } else { + tagName = jsonNode.get("name").asText(); + } + this.tags.add(tagName); + } + } + } + + public Boolean isNotification() { + return notification; + } + + public void setNotification(Boolean notification) { + this.notification = notification; + } + + @Override + public void run() { + + try { + + CatalogueConfiguration instance = CatalogueConfigurationFactory.getInstance(); + + if(!instance.isSocialPostEnabled()) { + logger.info("Social Post are disabled in the context {}", SecretManagerProvider.instance.get().getContext()); + return; + } + logger.info("Going to send Social Post about the Item {} available at {}", itemID, itemURL); + + boolean notifyUsers = instance.isNotificationToUsersEnabled(); + + if(notification != null) { + if(notifyUsers) { + notifyUsers = notifyUsers && notification; + }else { + notifyUsers = notification; + } + } + + // write notification post + sendSocialPost(notifyUsers); + + } catch(Exception e) { + logger.error("Error while executing post creation actions", e); + } + } + + public void sendSocialPost(boolean notifyUsers) { + SecretManager secretManager = SecretManagerProvider.instance.get(); + try { + StringWriter messageWriter = new StringWriter(); + messageWriter.append(String.format(NOTIFICATION_MESSAGE, userFullName, itemTitle, itemURL)); + + for(String tag : tags) { + tag = tag.trim(); + tag = tag.replaceAll(" ", "_").replace("_+", "_"); + if(tag.endsWith("_")) { + tag = tag.substring(0, tag.length() - 1); + } + messageWriter.append("#"); + messageWriter.append(tag); + messageWriter.append(" "); + } + String message = messageWriter.toString(); + + logger.debug("The social post that is going to be written is\n{}", message); + + Secret secret = Constants.getCatalogueSecret(); + secretManager.startSession(secret); + + PostClient postClient = new PostClient(); + PostInputBean postInputBean = new PostInputBean(); + postInputBean.setEnablenotification(notifyUsers); + postInputBean.setText(message); + Post post = postClient.writeApplicationPost(postInputBean); + logger.trace("Sent post {}", post); + + } catch(Exception e) { + logger.error("Unable to send Social Post", e); + } finally { + secretManager.endSession(); + } + + } + +} diff --git a/src/main/java/org/gcube/gcat/social/SocialUsers.java b/src/main/java/org/gcube/gcat/social/SocialUsers.java new file mode 100644 index 0000000..9dd56eb --- /dev/null +++ b/src/main/java/org/gcube/gcat/social/SocialUsers.java @@ -0,0 +1,19 @@ +package org.gcube.gcat.social; + +import java.util.HashSet; +import java.util.Set; + +import org.gcube.social_networking.social_networking_client_library.UserClient; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class SocialUsers { + + public static Set getUsernamesByRole(String roleName) throws Exception { + UserClient userClient = new UserClient(); + Set usernames = new HashSet<>(userClient.getAllUsernamesByRole(roleName)); + return usernames; + } + +} diff --git a/src/main/java/org/gcube/gcat/utils/Constants.java b/src/main/java/org/gcube/gcat/utils/Constants.java new file mode 100644 index 0000000..98ee410 --- /dev/null +++ b/src/main/java/org/gcube/gcat/utils/Constants.java @@ -0,0 +1,70 @@ +package org.gcube.gcat.utils; + +import java.io.InputStream; +import java.net.URL; +import java.util.AbstractMap.SimpleEntry; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.ws.rs.InternalServerErrorException; + +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.secret.JWTSecret; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.common.keycloak.KeycloakClientFactory; +import org.gcube.common.keycloak.model.TokenResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class Constants { + + private static final Logger logger = LoggerFactory.getLogger(Constants.class); + + public static final String CATALOGUE_NAME = "gCat"; + + protected static final String CLIENT_ID_SECRET_FILENAME = "config.properties"; + protected static final String CLIENT_ID_PROPERTY_NAME = "clientId"; + + private static Entry getClientIdAndClientSecret(String context) { + try { + Properties properties = new Properties(); + ClassLoader classLoader = Constants.class.getClassLoader(); + URL url = classLoader.getResource(CLIENT_ID_SECRET_FILENAME); + logger.trace("Going to read {} at {}", CLIENT_ID_SECRET_FILENAME, url.toString()); + InputStream input = classLoader.getResourceAsStream(CLIENT_ID_SECRET_FILENAME); + properties.load(input); + + String clientId = "gcat"; + if(properties.containsKey(CLIENT_ID_PROPERTY_NAME)) { + clientId = properties.getProperty(CLIENT_ID_PROPERTY_NAME); + } + + int index = context.indexOf('/', 1); + String root = context.substring(0, index == -1 ? context.length() : index); + String clientSecret = properties.getProperty(root); + + SimpleEntry entry = new SimpleEntry(clientId, clientSecret); + return entry; + } catch(Exception e) { + throw new InternalServerErrorException( + "Unable to retrieve Application Token for context " + SecretManagerProvider.instance.get().getContext(), e); + } + } + + private static TokenResponse getJWTAccessToken() throws Exception { + String context = SecretManagerProvider.instance.get().getContext(); + Entry entry = getClientIdAndClientSecret(context); + TokenResponse tr = KeycloakClientFactory.newInstance().queryUMAToken(context, entry.getKey(), entry.getValue(), context, null); + return tr; + } + + public static Secret getCatalogueSecret() throws Exception { + TokenResponse tr = getJWTAccessToken(); + Secret secret = new JWTSecret(tr.getAccessToken()); + return secret; + } + +} diff --git a/src/main/java/org/gcube/gcat/utils/HTTPCall.java b/src/main/java/org/gcube/gcat/utils/HTTPCall.java new file mode 100644 index 0000000..dfc0d57 --- /dev/null +++ b/src/main/java/org/gcube/gcat/utils/HTTPCall.java @@ -0,0 +1,90 @@ +package org.gcube.gcat.utils; + +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response.Status; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class HTTPCall { + + protected static final String USER_AGENT_KEY = "User-Agent"; + protected static final String USER_AGENT_NAME = "gCat"; + + protected final String address; + + /** + * When the target service is a gCube Service it adds the HTTP header + * to provide gCube authorization token and/or scope + */ + protected boolean gCubeTargetService; + + public boolean isgCubeTargetService() { + return gCubeTargetService; + } + + public void setgCubeTargetService(boolean gCubeTargetService) { + this.gCubeTargetService = gCubeTargetService; + } + + public HTTPCall(String address) { + this(address, HTTPCall.USER_AGENT_NAME); + } + + protected HTTPCall(String address, String userAgent) { + this.address = address; + this.gCubeTargetService = true; + } + + protected URL getURL(String urlString) throws MalformedURLException { + URL url = new URL(urlString); + if(url.getProtocol().compareTo("https") == 0) { + url = new URL(url.getProtocol(), url.getHost(), url.getDefaultPort(), url.getFile()); + } + return url; + } + + public URL getFinalURL(URL url) { + try { + URL finalURL = url; + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_NAME); + connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(1)); + // connection.setRequestMethod(HEAD.class.getSimpleName()); + + int responseCode = connection.getResponseCode(); + + if(responseCode >= Status.BAD_REQUEST.getStatusCode()) { + Status status = Status.fromStatusCode(responseCode); + String responseMessage = connection.getResponseMessage(); + throw new WebApplicationException(responseMessage, status); + } + + if(responseCode == HttpURLConnection.HTTP_MOVED_TEMP || responseCode == HttpURLConnection.HTTP_MOVED_PERM + || responseCode == HttpURLConnection.HTTP_SEE_OTHER + || responseCode == Status.TEMPORARY_REDIRECT.getStatusCode() || responseCode == 308) { + + URL newURL = getURL(connection.getHeaderField("Location")); + connection.disconnect(); + finalURL = getFinalURL(newURL); + } + + return finalURL; + + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new InternalServerErrorException(e); + } + + } + +} diff --git a/src/main/java/org/gcube/gcat/utils/HTTPUtility.java b/src/main/java/org/gcube/gcat/utils/HTTPUtility.java new file mode 100644 index 0000000..1c93735 --- /dev/null +++ b/src/main/java/org/gcube/gcat/utils/HTTPUtility.java @@ -0,0 +1,65 @@ +package org.gcube.gcat.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; + +import org.gcube.common.gxhttp.request.GXHTTPStringRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class HTTPUtility { + + private static final Logger logger = LoggerFactory.getLogger(HTTPUtility.class); + + public static StringBuilder getStringBuilder(InputStream inputStream) throws IOException { + StringBuilder result = new StringBuilder(); + try(BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while((line = reader.readLine()) != null) { + result.append(line); + } + } + + return result; + } + + public static GXHTTPStringRequest createGXHTTPStringRequest(String url, String path, boolean post) + throws UnsupportedEncodingException { + GXHTTPStringRequest gxhttpStringRequest = GXHTTPStringRequest.newRequest(url); + gxhttpStringRequest.from(Constants.CATALOGUE_NAME); + if(post) { + gxhttpStringRequest.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + } + gxhttpStringRequest.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); + gxhttpStringRequest.path(path); + return gxhttpStringRequest; + } + + public static String getResultAsString(HttpURLConnection httpURLConnection) throws IOException { + int responseCode = httpURLConnection.getResponseCode(); + if(responseCode >= Status.BAD_REQUEST.getStatusCode()) { + Status status = Status.fromStatusCode(responseCode); + InputStream inputStream = httpURLConnection.getErrorStream(); + StringBuilder result = getStringBuilder(inputStream); + logger.trace(result.toString()); + throw new WebApplicationException(result.toString(), status); + } + InputStream inputStream = httpURLConnection.getInputStream(); + String ret = getStringBuilder(inputStream).toString(); + logger.trace("Got Respose is {}", ret); + return ret; + } + +} diff --git a/src/main/java/org/gcube/gcat/utils/RandomString.java b/src/main/java/org/gcube/gcat/utils/RandomString.java new file mode 100644 index 0000000..8077878 --- /dev/null +++ b/src/main/java/org/gcube/gcat/utils/RandomString.java @@ -0,0 +1,37 @@ +package org.gcube.gcat.utils; + +import java.util.Random; + +/** + * @author Lucio Lelii (ISTI - CNR) + * @author Luca Frosini (ISTI - CNR) + */ +public class RandomString { + + private static final char[] symbols; + + static { + StringBuilder tmp = new StringBuilder(); + for(char ch = '0'; ch <= '9'; ++ch) + tmp.append(ch); + for(char ch = 'a'; ch <= 'z'; ++ch) + tmp.append(ch); + symbols = tmp.toString().toCharArray(); + } + + private final Random random = new Random(); + + private final char[] buf; + + public RandomString(int length) { + if(length < 1) + throw new IllegalArgumentException("length < 1: " + length); + buf = new char[length]; + } + + public String nextString() { + for(int idx = 0; idx < buf.length; ++idx) + buf[idx] = symbols[random.nextInt(symbols.length)]; + return new String(buf); + } +} diff --git a/src/main/java/org/gcube/gcat/utils/URIResolver.java b/src/main/java/org/gcube/gcat/utils/URIResolver.java new file mode 100644 index 0000000..d30830d --- /dev/null +++ b/src/main/java/org/gcube/gcat/utils/URIResolver.java @@ -0,0 +1,66 @@ +package org.gcube.gcat.utils; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.WebApplicationException; + +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.portlets.user.uriresolvermanager.UriResolverManager; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class URIResolver { + + private static final String CATALOGUE_CONTEXT = "gcube_scope"; + private static final String ENTITY_TYPE = "entity_context"; + private static final String ENTITY_NAME = "entity_name"; + + private static final String DATASET = "dataset"; + + protected static URIResolver uriResolver; + + protected UriResolverManager uriResolverManager; + protected Calendar expireTime; + + public static URIResolver getInstance() { + if(uriResolver == null) { + uriResolver = new URIResolver(); + }else { + Calendar now = Calendar.getInstance(); + if(now.after(uriResolver.expireTime)) { + uriResolver = new URIResolver(); + } + } + return uriResolver; + } + + private URIResolver() { + try { + uriResolverManager = new UriResolverManager("CTLG"); + expireTime = Calendar.getInstance(); + expireTime.add(Calendar.MINUTE, 30); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getCatalogueItemURL(String name) { + try { + String context = SecretManagerProvider.instance.get().getContext(); + Map params = new HashMap<>(); + params.put(CATALOGUE_CONTEXT, context); + params.put(ENTITY_TYPE, DATASET); + params.put(ENTITY_NAME, name); + String url = uriResolverManager.getLink(params, false); + return url; + } catch(WebApplicationException e) { + throw e; + } catch(Exception e) { + throw new WebApplicationException(e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/gcube/gcat/workspace/GcatMetadataMatcher.java b/src/main/java/org/gcube/gcat/workspace/GcatMetadataMatcher.java new file mode 100644 index 0000000..81a1075 --- /dev/null +++ b/src/main/java/org/gcube/gcat/workspace/GcatMetadataMatcher.java @@ -0,0 +1,40 @@ +package org.gcube.gcat.workspace; + +import java.util.HashMap; +import java.util.Map; + +import org.gcube.common.storagehub.model.Metadata; +import org.gcube.gcat.utils.Constants; +import org.gcube.storagehub.MetadataMatcher; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GcatMetadataMatcher extends MetadataMatcher { + + public static final String GCAT_METADATA_VERSION = "2.0.0"; + + public static final String CATALOGUE_ITEM_ID = "CatalogueItemID"; + + public GcatMetadataMatcher(String id) { + super(Constants.CATALOGUE_NAME, GCAT_METADATA_VERSION, id); + } + + @Override + public boolean check(Metadata metadata) { + Map map = metadata.getMap(); + Object obj = map.get(CATALOGUE_ITEM_ID); + if(obj!=null && obj.toString().compareTo(id) == 0) { + return true; + } + return false; + } + + @Override + protected Map getSpecificMetadataMap() { + Map map = new HashMap<>(); + map.put(CATALOGUE_ITEM_ID, id); + return map; + } + +} diff --git a/src/main/java/org/gcube/gcat/workspace/GcatStorageHubManagement.java b/src/main/java/org/gcube/gcat/workspace/GcatStorageHubManagement.java new file mode 100644 index 0000000..2bc4fc3 --- /dev/null +++ b/src/main/java/org/gcube/gcat/workspace/GcatStorageHubManagement.java @@ -0,0 +1,164 @@ +package org.gcube.gcat.workspace; + +import java.net.HttpURLConnection; +import java.net.URL; + +import org.gcube.common.authorization.utils.manager.SecretManager; +import org.gcube.common.authorization.utils.manager.SecretManagerProvider; +import org.gcube.common.authorization.utils.secret.Secret; +import org.gcube.common.gxhttp.request.GXHTTPStringRequest; +import org.gcube.common.storagehub.client.dsl.FileContainer; +import org.gcube.gcat.utils.Constants; +import org.gcube.storagehub.MetadataMatcher; +import org.gcube.storagehub.StorageHubManagement; +import org.glassfish.jersey.media.multipart.ContentDisposition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Luca Frosini (ISTI - CNR) + */ +public class GcatStorageHubManagement { + + private static final Logger logger = LoggerFactory.getLogger(GcatStorageHubManagement.class); + + protected StorageHubManagement storageHubManagement; + + protected String itemId; + protected String originalFilename; + protected String mimeType; + + public String getOriginalFilename() { + return originalFilename; + } + + public String getMimeType() { + return mimeType; + } + + public GcatStorageHubManagement() { + this.storageHubManagement = new StorageHubManagement(); + } + + protected MetadataMatcher getMetadataMatcher() { + MetadataMatcher metadataMatcher = new GcatMetadataMatcher(itemId); + return metadataMatcher; + } + + protected String getOriginalFileName(HttpURLConnection httpURLConnection) throws Exception { + String contentDisposition = httpURLConnection.getHeaderField("Content-Disposition"); + contentDisposition = contentDisposition.replaceAll("= ", "=").replaceAll(" =", "="); + ContentDisposition formDataContentDisposition = new ContentDisposition(contentDisposition); + return formDataContentDisposition.getFileName(); + } + + public URL ensureResourcePersistence(URL persistedURL, String itemID, String resourceID) throws Exception { + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + try { + secretManager.startSession(secret); + GXHTTPStringRequest gxhttpStringRequest = GXHTTPStringRequest.newRequest(persistedURL.toString()); + gxhttpStringRequest.from(Constants.CATALOGUE_NAME); + gxhttpStringRequest.isExternalCall(true); + HttpURLConnection httpURLConnection = gxhttpStringRequest.get(); + mimeType = httpURLConnection.getContentType().split(";")[0]; + originalFilename = getOriginalFileName(httpURLConnection); + + this.itemId = itemID; + storageHubManagement.setMetadataMatcher(getMetadataMatcher()); + + persistedURL = storageHubManagement.persistFile(httpURLConnection.getInputStream(), resourceID, mimeType); + return persistedURL; + } catch (Exception e) { + logger.error("Error while trying to persists the resource", e); + throw e; + } finally { + secretManager.endSession(); + } + } + + public void deleteResourcePersistence(String itemID, String resourceID, String mimeType) throws Exception { + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + try { + secretManager.startSession(secret); + storageHubManagement = new StorageHubManagement(); + this.itemId = itemID; + storageHubManagement.setMetadataMatcher(getMetadataMatcher()); + storageHubManagement.removePersistedFile(resourceID, mimeType); + } finally { + secretManager.endSession(); + } + } + +// protected void internalAddRevisionID(String resourceID, String revisionID) throws Exception { +// try { +// FileContainer fileContainer = null; +// AbstractFileItem fileItem = null; +// try { +// fileContainer = storageHubManagement.getPersistedFile(); +// fileItem = fileContainer.get(); +// }catch (Exception e) { +// // This is a workaround because storage-hub invalidate the item +// // when I rename it (just before this operation) +// // then I get java.lang.RuntimeException: javax.ws.rs.ProcessingException: Error reading entity from input stream. +// // invoking fileContainer.get() +// // see issue #25373 +// fileContainer = storageHubManagement.getPersistedFile(resourceID, mimeType); +// fileItem = fileContainer.get(); +// } +// Metadata metadata = fileItem.getMetadata(); +// Map map = metadata.getMap(); +// map.put(CatalogueMetadata.CATALOGUE_RESOURCE_ID, resourceID); +// map.put(CatalogueMetadata.CATALOGUE_RESOURCE_REVISION_ID, revisionID); +// metadata.setMap(map); +// fileContainer.setMetadata(metadata); +// } catch (Exception e) { +// logger.warn( +// "Unable to set revision id {} to the file of resource with id {} because the file was NOT found on storage-hub.", +// revisionID, resourceID); +// throw e; +// } +// } +// public void renameFile(String resourceID, String revisionID) throws Exception { + public void renameFile(String resourceID) throws Exception { + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + try { + secretManager.startSession(secret); + FileContainer createdfile = storageHubManagement.getPersistedFile(); + createdfile.rename(resourceID); +// internalAddRevisionID(resourceID, revisionID); + } finally { + secretManager.endSession(); + } + + } + +// public void addRevisionID(String resourceID, String revisionID) throws Exception { +// SecretManager secretManager = SecretManagerProvider.instance.get(); +// Secret secret = Constants.getCatalogueSecret(); +// try { +// secretManager.startSession(secret); +// internalAddRevisionID(resourceID, revisionID); +// } finally { +// secretManager.endSession(); +// } +// } + + public FileContainer retrievePersistedFile(String filename, String mimeType) throws Exception { + SecretManager secretManager = SecretManagerProvider.instance.get(); + Secret secret = Constants.getCatalogueSecret(); + try { + secretManager.startSession(secret); + return storageHubManagement.getPersistedFile(filename, mimeType); + } finally { + secretManager.endSession(); + } + } + + public FileContainer getPersistedFile() throws Exception { + return storageHubManagement.getPersistedFile(); + } + +}