diff --git a/README.adoc b/README.adoc index 1db8fa0930..57c3d354bc 100644 --- a/README.adoc +++ b/README.adoc @@ -2238,10 +2238,10 @@ The data models for these metadata are defined using the {SHACL}[Shapes Constrai * The customisable data model includes the custom (shared) metadata entities, custom controlled vocabulary types, and custom properties of the system entities. - The default custom data model is defined in link:projects/saturn/vocabulary.ttl[vocabulary.ttl]. + The default custom data model is defined in link:projects/saturn/src/main/resources/vocabulary.ttl[vocabulary.ttl]. This data model can be overriden by a data more suitable for your organisation. -A schematic overview of the default data model in link:projects/saturn/vocabulary.ttl[vocabulary.ttl]: +A schematic overview of the default data model in link:projects/saturn/src/main/resources/vocabulary.ttl[vocabulary.ttl]: image:docs/images/diagrams/CDR data model.png[CDR data model] @@ -2462,8 +2462,8 @@ use `--set-file` option: + This should also restart the Saturn pod. If not, trigger the restart manually. + -For local development - replace vocabulary file in link:projects/saturn/vocabulary.ttl[projects/saturn/vocabulary.ttl] -and views configuration in link:projects/saturn/views.ttl[projects/saturn/views.ttl]. +For local development - replace vocabulary file in link:projects/saturn/src/main/resources/vocabulary.ttl[vocabulary.ttl] +and views configuration in link:projects/saturn/src/main/resources/views.yaml[views.yaml]. Restart Saturn run. + . Load data for new entities or properties. @@ -2474,7 +2474,7 @@ Restart Saturn run. For controlled vocabulary types, e.g., _Gender_ and _Species_ in the example, you should insert the allowed values in the database by uploading a taxonomies file using the <> API. -An example taxonomy is in link:projects/saturn/taxonomies.ttl[taxonomies.ttl]. +An example taxonomy is in link:projects/saturn/src/main/resources/taxonomies.ttl[taxonomies.ttl]. It is preferred to use existing standard taxonomies and labels. If that is not possible, please define your own namespaces for @@ -2526,7 +2526,7 @@ ncbitaxon:10090 a example:Species ; For the metadata pages in the user interface, a view configuration needs to be created that specifies the tables and columns. -An example can be found in link:projects/saturn/views.yaml[views.yaml] +An example can be found in link:projects/saturn/src/main/resources/views.yaml[views.yaml] diff --git a/charts/fairspace/templates/project/stateful-set.yaml b/charts/fairspace/templates/project/stateful-set.yaml index 79fa344454..a9468cb306 100644 --- a/charts/fairspace/templates/project/stateful-set.yaml +++ b/charts/fairspace/templates/project/stateful-set.yaml @@ -100,7 +100,7 @@ spec: {{- end }} livenessProbe: httpGet: - path: /liveness + path: /actuator/health/liveness port: 8091 initialDelaySeconds: {{ .Values.saturn.livenessProbe.initialDelaySeconds }} periodSeconds: {{ .Values.saturn.livenessProbe.periodSeconds }} @@ -108,8 +108,8 @@ spec: timeoutSeconds: {{ .Values.saturn.livenessProbe.timeoutSeconds }} readinessProbe: httpGet: - path: /api/health/ - port: 8090 + path: /actuator/health/readiness + port: 8091 periodSeconds: {{ .Values.saturn.readinessProbe.periodSeconds }} successThreshold: {{ .Values.saturn.readinessProbe.successThreshold }} timeoutSeconds: {{ .Values.saturn.readinessProbe.timeoutSeconds }} @@ -222,4 +222,4 @@ spec: storageClassName: {{ index .Values "saturn" "persistence" "extra-file-storage" "storageClass" | quote }} {{- end }} {{- end }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/projects/mercury/package.json b/projects/mercury/package.json index 1a13d312e5..ad35f08667 100644 --- a/projects/mercury/package.json +++ b/projects/mercury/package.json @@ -7,8 +7,8 @@ "dev-be": "concurrently \"yarn localdevelopment\" \"yarn saturn\" \"yarn pluto\"", "dev-fe": "yarn build && yarn start", "localdevelopment": "cd ../../local-development && docker compose up", - "saturn": "wait-on http://localhost:5100/ && wait-on tcp:9432 && cd ../saturn/ && KEYCLOAK_CLIENT_SECRET=********** ./gradlew run", - "pluto": "wait-on http://localhost:8090/api/health/ && cd ../pluto/ && KEYCLOAK_CLIENT_SECRET=********** ./gradlew bootRun --args='--spring.profiles.active=local'", + "saturn": "wait-on http://localhost:5100/ && wait-on tcp:9432 && cd ../saturn/ && KEYCLOAK_CLIENT_SECRET=********** ./gradlew bootRun", + "pluto": "wait-on http://localhost:8091/actuator/health/ && cd ../pluto/ && KEYCLOAK_CLIENT_SECRET=********** ./gradlew bootRun --args='--spring.profiles.active=local'", "start": "craco start", "build": "yarn check-format && craco build", "test": "craco test --env=jsdom", diff --git a/projects/pluto/src/main/resources/application-local.yml b/projects/pluto/src/main/resources/application-local.yml index 7c8e22eec2..f668877f9d 100644 --- a/projects/pluto/src/main/resources/application-local.yml +++ b/projects/pluto/src/main/resources/application-local.yml @@ -4,7 +4,7 @@ pluto: domains: - http://localhost:8080 force-https: false - downstreamServiceHealthUrl: http://localhost:8090/api/health/ + downstreamServiceHealthUrl: http://localhost:8091/actuator/health/ oauth2: base-url: http://localhost:5100 realm: fairspace diff --git a/projects/saturn/application.yaml b/projects/saturn/application.yaml deleted file mode 100644 index 9dbebe53e0..0000000000 --- a/projects/saturn/application.yaml +++ /dev/null @@ -1,68 +0,0 @@ -# Application's port -port: 8090 -livenessPort: 8091 -publicUrl: http://localhost:8080 -jena: - # Base IRI for all metadata entities - metadataBaseIRI: "http://localhost/iri/" - # Jena's TDB2 database path - datasetPath: "data/db" - storeParams: - tdb.file_mode: "mapped" - tdb.block_size: 8192 - tdb.block_read_cache_size: 5000 - tdb.block_write_cache_size: 1000 - tdb.node2nodeid_cache_size: 200000 - tdb.nodeid2node_cache_size: 750000 - tdb.node_miss_cache_size: 1000 - tdb.nodetable: "nodes" - tdb.triple_index_primary: "SPO" - tdb.triple_indexes: - - "SPO" - - "POS" - - "OSP" - tdb.quad_index_primary: "GSPO" - tdb.quad_indexes: - - "GSPO" - - "GPOS" - - "GOSP" - - "POSG" - - "OSPG" - - "SPOG" - tdb.prefixtable: "prefixes" - tdb.prefix_index_primary: "GPU" - tdb.prefix_indexes: - - "GPU" - # Path of the transaction log - transactionLogPath: "data/log" - bulkTransactions: true -auth: - authServerUrl: http://localhost:5100/ - realm: fairspace - clientId: workspace-client - enableBasicAuth: true - superAdminUser: organisation-admin - defaultUserRoles: - - canViewPublicMetadata -webDAV: - # Path of the WebDAV's local blob store - blobStorePath: "data/blobs" -features: - - ExtraStorage -viewDatabase: - enabled: true - url: jdbc:postgresql://localhost:9432/fairspace - autoCommit: false - maxPoolSize: 50 - connectionTimeout: 1000 - mvRefreshOnStartRequired: true -search: - pageRequestTimeout: 10000 - countRequestTimeout: 60000 - maxJoinItems: 50 -caches: - facets: - name: "facets" - views: - name: "views" - diff --git a/projects/saturn/build.gradle b/projects/saturn/build.gradle index d57939c6e2..2cdf612308 100644 --- a/projects/saturn/build.gradle +++ b/projects/saturn/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - jena_version = '4.10.0' // todo: upgrade to 5+ (FAIRSPC-69) - milton_version = '3.1.1.488' // Milton >= 4 is migrated to Jakarta EE 9, which is not compatible with Jena < 5 (no stable release of Jena 5 yet). To be updated when Jena 5 is released. + jena_version = '5.1.0' + milton_version = '4.0.2.2101' mockitoVersion = '5.11.0' jacksonVersion = '2.15.3' // check what version is used by Jena postgresqlVersion = '42.7.2' @@ -10,6 +10,8 @@ buildscript { plugins { id 'java' + id 'org.springframework.boot' version '3.3.0' + id 'io.spring.dependency-management' version '1.1.5' id "io.freefair.lombok" version "8.6" id 'application' id 'jacoco' @@ -26,7 +28,7 @@ compileJava { } application { - mainClassName = "io.fairspace.saturn.App" + mainClassName = "io.fairspace.saturn.SaturnApplication" applicationDefaultJvmArgs = ['-XX:+ShowCodeDetailsInExceptionMessages'] } @@ -38,21 +40,25 @@ repositories { } } -lombok.version = "1.18.30" +lombok.version = "1.18.32" jacoco.toolVersion = "0.8.11" dependencies { -// implementation fileTree(dir: 'libs', include: '*.jar') // jena-fuseki-server-*.jar jena-text-es-*.jar - implementation "org.apache.jena:jena-fuseki-main:${jena_version}" + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation "org.apache.jena:jena-text:${jena_version}" implementation "io.milton:milton-server-ce:${milton_version}" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonVersion}" implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - implementation "com.sparkjava:spark-core:2.9.4" + implementation 'com.pivovarit:throwing-function:1.5.1' implementation 'com.google.guava:guava:33.0.0-jre' -// implementation 'jakarta.servlet:jakarta.servlet-api:6.0.0' // To be updated when Jena 5 is released. implementation('com.io-informatics.oss:jackson-jsonld:0.1.1') { exclude group: 'com.github.jsonld-java' } @@ -60,31 +66,21 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-layout-template-json:2.23.0' - implementation 'org.eclipse.jetty:jetty-proxy:9.4.54.v20240208' - - implementation 'org.keycloak:keycloak-jetty94-adapter:23.0.7' implementation 'org.keycloak:keycloak-admin-client:23.0.7' implementation 'org.keycloak:keycloak-policy-enforcer:23.0.7' implementation "org.postgresql:postgresql:${postgresqlVersion}" implementation "com.zaxxer:HikariCP:5.1.0" - runtimeOnly 'org.apache.logging.log4j:log4j-api:2.23.0' - runtimeOnly 'org.apache.logging.log4j:log4j-core:2.23.0' - runtimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl:2.23.0' - testImplementation "junit:junit:4.13.2" testImplementation "org.mockito:mockito-core:${mockitoVersion}" + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.junit.vintage:junit-vintage-engine:5.10.2' testImplementation "org.testcontainers:postgresql:1.19.6" testImplementation('com.github.stefanbirkner:system-rules:1.19.0') { exclude group: 'junit', module:'junit-dep' } - constraints { -// implementation('com.fasterxml.jackson.core:jackson-databind:2.9.10.1') { -// because 'previous versions have security vulnerabilities' -// } - } } jacocoTestReport { @@ -117,4 +113,5 @@ test { // They also blocks usage of testing library mocking environment variables, // That is why this additional arg for tests is needed jvmArgs = ['--add-opens', 'java.base/java.util=ALL-UNNAMED'] + useJUnitPlatform() } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/App.java b/projects/saturn/src/main/java/io/fairspace/saturn/App.java deleted file mode 100644 index e9ccf8a86f..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/App.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.fairspace.saturn; - -import java.sql.*; - -import lombok.extern.log4j.*; -import org.apache.jena.fuseki.main.FusekiServer; -import org.apache.jena.fuseki.server.*; -import org.apache.jena.riot.*; -import org.eclipse.jetty.server.session.SessionHandler; - -import io.fairspace.saturn.auth.*; -import io.fairspace.saturn.config.*; -import io.fairspace.saturn.rdf.SaturnDatasetFactory; -import io.fairspace.saturn.services.views.*; - -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; -import static io.fairspace.saturn.config.ConfigLoader.VIEWS_CONFIG; -import static io.fairspace.saturn.config.SparkFilterFactory.createSparkFilter; - -@Log4j2 -public class App { - public static final String API_PREFIX = "/api"; - - public static FusekiServer startFusekiServer() { - log.info("Saturn is starting"); - ViewStoreClientFactory viewStoreClientFactory = null; - if (CONFIG.viewDatabase.enabled) { - try { - viewStoreClientFactory = new ViewStoreClientFactory(VIEWS_CONFIG, CONFIG.viewDatabase, CONFIG.search); - } catch (SQLException e) { - log.error("Error connecting to the view database.", e); - throw new RuntimeException("Error connecting to the view database", e); // Terminates Saturn - } - } - var ds = SaturnDatasetFactory.connect(CONFIG.jena, viewStoreClientFactory); - - var svc = new Services(CONFIG, VIEWS_CONFIG, ds, viewStoreClientFactory); - - var operationRegistry = OperationRegistry.createStd(); - operationRegistry.register( - Operation.Query, - WebContent.contentTypeSPARQLQuery, - new Protected_SPARQL_QueryDataset(svc.getUserService())); - var serverBuilder = FusekiServer.create(operationRegistry) - .securityHandler(new SaturnSecurityHandler(CONFIG.auth)) - .add(API_PREFIX + "/rdf/", svc.getFilteredDatasetGraph(), false) - .addServlet(API_PREFIX + "/webdav/*", svc.getDavServlet()) - .addFilter("/*", createSparkFilter(API_PREFIX, svc, CONFIG)) - .port(CONFIG.port); - if (CONFIG.features.contains(Feature.ExtraStorage)) { - serverBuilder.addServlet(API_PREFIX + "/extra-storage/*", svc.getExtraDavServlet()); - } - - var server = serverBuilder.build(); - server.getJettyServer().insertHandler(new SessionHandler()); - server.start(); - log.info("Saturn has started"); - return server; - } - - public static void main(String[] args) { - try (var ignored = new LivenessServer()) { - var fusekiServer = startFusekiServer(); - fusekiServer.join(); - } catch (Throwable e) { - throw new RuntimeException("Saturn ended unexpectedly", e); - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/SaturnApplication.java b/projects/saturn/src/main/java/io/fairspace/saturn/SaturnApplication.java new file mode 100644 index 0000000000..8a98d0d056 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/SaturnApplication.java @@ -0,0 +1,12 @@ +package io.fairspace.saturn; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SaturnApplication { + + public static void main(String[] args) { + SpringApplication.run(SaturnApplication.class, args); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/audit/Audit.java b/projects/saturn/src/main/java/io/fairspace/saturn/audit/Audit.java index c78569138e..efbab69177 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/audit/Audit.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/audit/Audit.java @@ -2,10 +2,11 @@ import java.util.Objects; -import org.apache.logging.log4j.*; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; -import static io.fairspace.saturn.auth.RequestContext.getAccessToken; +import static io.fairspace.saturn.auth.RequestContext.getClaims; public class Audit { private static final Logger log = LogManager.getLogger("audit"); @@ -19,16 +20,18 @@ public static void audit(String event, Object... params) { } } - var token = getAccessToken(); + var claims = getClaims(); + String preferredUsername = claims.getPreferredUsername(); - if (token.getPreferredUsername() != null) { - ThreadContext.put("user_name", token.getPreferredUsername()); + if (preferredUsername != null) { + ThreadContext.put("user_name", preferredUsername); } - if (token.getEmail() != null) { - ThreadContext.put("user_email", token.getEmail()); + String email = claims.getEmail(); + if (email != null) { + ThreadContext.put("user_email", email); } - ThreadContext.put("user_id", token.getSubject()); + ThreadContext.put("user_id", claims.getSubject()); log.trace(event); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/CustomAuthenticationEntryPoint.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000000..cee285dcf3 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/CustomAuthenticationEntryPoint.java @@ -0,0 +1,29 @@ +package io.fairspace.saturn.auth; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + public static final String LOGIN_PATH = "/login"; + + @Override + public void commence( + HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException { + if (request.getRequestURI().startsWith("/api/")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + if (request.getCookies() == null || request.getCookies().length == 0) { + response.addHeader("WWW-Authenticate", "Basic"); + } + } else { + response.sendRedirect(LOGIN_PATH); + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverter.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverter.java new file mode 100644 index 0000000000..e446bf75d7 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverter.java @@ -0,0 +1,59 @@ +package io.fairspace.saturn.auth; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthConverter implements Converter { + + private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + + private final JwtAuthConverterProperties properties; + + public JwtAuthConverter(JwtAuthConverterProperties properties) { + this.properties = properties; + } + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + Collection authorities = Stream.concat( + jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractResourceRoles(jwt).stream()) + .collect(Collectors.toSet()); + return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt)); + } + + private String getPrincipalClaimName(Jwt jwt) { + String claimName = JwtClaimNames.SUB; + if (properties.getPrincipalAttribute() != null) { + claimName = properties.getPrincipalAttribute(); + } + return jwt.getClaim(claimName); + } + + private Collection extractResourceRoles(Jwt jwt) { + Map resourceAccess = jwt.getClaim("resource_access"); + Map resource; + Collection resourceRoles; + if (resourceAccess == null + || (resource = (Map) resourceAccess.get(properties.getResourceId())) == null + || (resourceRoles = (Collection) resource.get("roles")) == null) { + return Set.of(); + } + return resourceRoles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .collect(Collectors.toSet()); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverterProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverterProperties.java new file mode 100644 index 0000000000..51e3718fdb --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/JwtAuthConverterProperties.java @@ -0,0 +1,15 @@ +package io.fairspace.saturn.auth; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "jwt.auth.converter") +public class JwtAuthConverterProperties { + + private String resourceId; + + private String principalAttribute; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/RequestContext.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/RequestContext.java index 9895232662..07529831f3 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/auth/RequestContext.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/RequestContext.java @@ -1,30 +1,34 @@ package io.fairspace.saturn.auth; -import java.security.Principal; +import java.util.Map; import java.util.Optional; +import jakarta.servlet.http.HttpServletRequest; +import lombok.EqualsAndHashCode; import org.apache.jena.graph.Node; -import org.eclipse.jetty.server.*; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.representations.AccessToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtClaimAccessor; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import io.fairspace.saturn.rdf.SparqlUtils; public class RequestContext { - private static final ThreadLocal currentRequest = new ThreadLocal<>(); + private static final ThreadLocal currentRequest = new ThreadLocal<>(); private static final ThreadLocal currentUserUri = new ThreadLocal<>(); - public static Request getCurrentRequest() { - return Optional.ofNullable(HttpConnection.getCurrentConnection()) - .map(HttpConnection::getHttpChannel) - .map(HttpChannel::getRequest) + public static HttpServletRequest getCurrentRequest() { + return Optional.ofNullable(RequestContextHolder.getRequestAttributes()) + .map(ServletRequestAttributes.class::cast) + .map(ServletRequestAttributes::getRequest) .orElseGet(currentRequest::get); } - public static void setCurrentRequest(Request request) { + public static void setCurrentRequest(HttpServletRequest request) { currentRequest.set(request); } @@ -40,37 +44,61 @@ public static void setCurrentUserStringUri(String uri) { currentUserUri.set(uri); } - private static Optional getUserIdentity() { - return Optional.ofNullable(getCurrentRequest()) - .map(Request::getAuthentication) - .map(x -> (Authentication.User) x) - .map(Authentication.User::getUserIdentity); + public static Node getUserURI() { + return getJwt().map(JwtClaimAccessor::getSubject) + .map(SparqlUtils::generateMetadataIriFromId) + .or(() -> Optional.ofNullable(currentUserUri.get()).map(SparqlUtils::generateMetadataIriFromUri)) + .orElse(null); } - private static Optional getPrincipal() { - return getUserIdentity().map(UserIdentity::getUserPrincipal); + public static SaturnClaims getClaims() { + return getJwt().map(Jwt::getClaims).map(SaturnClaims::from).orElseGet(SaturnClaims::emptyClaims); } - public static Node getUserURI() { - return getPrincipal() - .map(Principal::getName) - .map(SparqlUtils::generateMetadataIri) - .orElse(null); + private static Optional getAuthentication() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()); } - public static AccessToken getAccessToken() { - return getPrincipal() - .map(x -> (KeycloakPrincipal) x) - .map(KeycloakPrincipal::getKeycloakSecurityContext) - .map(KeycloakSecurityContext::getToken) - .orElse(null); + private static Optional getJwt() { + return getAuthentication().map(Authentication::getPrincipal).map(Jwt.class::cast); } - public static String getIdTokenString() { - return getPrincipal() - .map(x -> (KeycloakPrincipal) x) - .map(KeycloakPrincipal::getKeycloakSecurityContext) - .map(KeycloakSecurityContext::getIdTokenString) - .orElse(null); + @EqualsAndHashCode + public static class SaturnClaims { + + private static final String PREFERRED_USERNAME = "preferred_username"; + private static final String SUBJECT = "sub"; + private static final String EMAIL = "email"; + private static final String NAME = "name"; + + private final Map claims; + + private SaturnClaims(Map claims) { + this.claims = claims; + } + + public static SaturnClaims from(Map claims) { + return new SaturnClaims(claims); + } + + public static SaturnClaims emptyClaims() { + return new SaturnClaims(Map.of()); + } + + public String getPreferredUsername() { + return (String) claims.get(PREFERRED_USERNAME); + } + + public String getSubject() { + return (String) claims.get(SUBJECT); + } + + public String getEmail() { + return (String) claims.get(EMAIL); + } + + public String getName() { + return (String) claims.get(NAME); + } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnKeycloakJettyAuthenticator.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnKeycloakJettyAuthenticator.java deleted file mode 100644 index ded2d701f4..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnKeycloakJettyAuthenticator.java +++ /dev/null @@ -1,186 +0,0 @@ -package io.fairspace.saturn.auth; - -import java.io.*; -import java.util.*; -import javax.security.cert.*; -import javax.servlet.ServletRequest; - -import org.eclipse.jetty.http.*; -import org.eclipse.jetty.server.Request; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.adapters.AdapterTokenStore; -import org.keycloak.adapters.KeycloakDeployment; -import org.keycloak.adapters.RefreshableKeycloakSecurityContext; -import org.keycloak.adapters.jetty.Jetty94RequestAuthenticator; -import org.keycloak.adapters.jetty.KeycloakJettyAuthenticator; -import org.keycloak.adapters.jetty.core.JettyRequestAuthenticator; -import org.keycloak.adapters.jetty.spi.JettyHttpFacade; -import org.keycloak.adapters.spi.*; -import org.keycloak.common.util.*; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; - -class SaturnKeycloakJettyAuthenticator extends KeycloakJettyAuthenticator { - /** - * Reads the X-Forwarded-Proto header from the request, if set, - * and uses that value to determine the URI and the security of the request. - */ - static class ProxiedRequestWrapper implements HttpFacade.Request { - final HttpFacade.Request request; - - public ProxiedRequestWrapper(HttpFacade.Request request) { - this.request = request; - } - - @Override - public String getMethod() { - return request.getMethod(); - } - - @Override - public String getURI() { - var uri = request.getURI(); - var uriBuilder = KeycloakUriBuilder.fromUri(uri); - if (isSecure() && HttpScheme.HTTP.is(uriBuilder.getScheme())) { - return uriBuilder.scheme("https").port(-1).build().toString(); - } - return request.getURI(); - } - - @Override - public String getRelativePath() { - return request.getRelativePath(); - } - - @Override - public boolean isSecure() { - return request.isSecure() || HttpScheme.HTTPS.is(getHeader("x-forwarded-proto")); - } - - @Override - public String getFirstParam(String param) { - return request.getFirstParam(param); - } - - @Override - public String getQueryParamValue(String param) { - return request.getQueryParamValue(param); - } - - @Override - public HttpFacade.Cookie getCookie(String cookieName) { - return request.getCookie(cookieName); - } - - @Override - public String getHeader(String name) { - return request.getHeader(name); - } - - @Override - public List getHeaders(String name) { - return request.getHeaders(name); - } - - @Override - public InputStream getInputStream() { - return request.getInputStream(); - } - - @Override - public InputStream getInputStream(boolean buffered) { - return request.getInputStream(buffered); - } - - @Override - public String getRemoteAddr() { - return request.getRemoteAddr(); - } - - @Override - public void setError(AuthenticationError error) { - request.setError(error); - } - - @Override - public void setError(LogoutError error) { - request.setError(error); - } - } - - static class ProxiedRequestFacadeWrapper implements HttpFacade { - final JettyHttpFacade facade; - - public ProxiedRequestFacadeWrapper(JettyHttpFacade facade) { - this.facade = facade; - } - - @Override - public Request getRequest() { - return new ProxiedRequestWrapper(facade.getRequest()); - } - - @Override - public Response getResponse() { - return facade.getResponse(); - } - - @SuppressWarnings("removal") - @Override - public X509Certificate[] getCertificateChain() { - return facade.getCertificateChain(); - } - } - - SaturnKeycloakJettyAuthenticator(AdapterConfig config) { - setAdapterConfig(config); - } - - @Override - protected JettyRequestAuthenticator createRequestAuthenticator( - Request request, JettyHttpFacade facade, KeycloakDeployment deployment, AdapterTokenStore tokenStore) { - return new Jetty94RequestAuthenticator( - new ProxiedRequestFacadeWrapper(facade), deployment, tokenStore, -1, request) { - @Override - public AuthChallenge getChallenge() { - // No redirects for API requests - if (request.getOriginalURI().startsWith("/api/")) { - return new AuthChallenge() { - @Override - public boolean challenge(HttpFacade exchange) { - if (deployment.isEnableBasicAuth() - && exchange.getRequest().getCookie("JSESSIONID") == null - && !exchange.getRequest() - .getHeader("X-Requested-With") - .equals("XMLHttpRequest")) { - exchange.getResponse().addHeader("WWW-Authenticate", "Basic"); - } - exchange.getResponse().setStatus(getResponseCode()); - return true; - } - - @Override - public int getResponseCode() { - return SC_UNAUTHORIZED; - } - }; - } - - return super.getChallenge(); - } - - @Override - protected void completeBearerAuthentication( - KeycloakPrincipal principal, String method) { - // Stores the token in the session - completeOAuthAuthentication(principal); - } - }; - } - - @Override - public void logout(ServletRequest request) { - logoutCurrent((Request) request); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnSecurityHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnSecurityHandler.java deleted file mode 100644 index 890a704d56..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/auth/SaturnSecurityHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.fairspace.saturn.auth; - -import java.util.Map; - -import lombok.extern.log4j.*; -import org.eclipse.jetty.security.ConstraintMapping; -import org.eclipse.jetty.security.ConstraintSecurityHandler; -import org.eclipse.jetty.util.security.Constraint; -import org.keycloak.common.enums.SslRequired; -import org.keycloak.enums.TokenStore; -import org.keycloak.representations.adapters.config.AdapterConfig; - -import io.fairspace.saturn.config.Config; - -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; - -import static java.lang.System.getenv; - -@Log4j2 -public class SaturnSecurityHandler extends ConstraintSecurityHandler { - public SaturnSecurityHandler(Config.Auth config) { - setAuthenticator(new SaturnKeycloakJettyAuthenticator(adapterConfig(config))); - addConstraintMapping(constraintMapping("/*", true)); - addConstraintMapping(constraintMapping("/api/health/", false)); - addConstraintMapping(constraintMapping("/favicon.ico", false)); - } - - private static AdapterConfig adapterConfig(Config.Auth config) { - var adapterConfig = new AdapterConfig(); - adapterConfig.setResource(config.clientId); - adapterConfig.setRealm(config.realm); - adapterConfig.setCors(true); - adapterConfig.setAuthServerUrl(CONFIG.auth.authServerUrl); - adapterConfig.setTokenStore(TokenStore.SESSION.name()); - adapterConfig.setCredentials(Map.of("secret", getenv("KEYCLOAK_CLIENT_SECRET"))); - adapterConfig.setEnableBasicAuth(config.enableBasicAuth); - - var unsecure = CONFIG.auth.authServerUrl.startsWith("http://"); - adapterConfig.setSslRequired(SslRequired.NONE.name()); - if (unsecure) { - log.warn( - "The Keycloak authServerUrl does not use HTTPS, which means it is not secure. Don't do this in production!"); - } else { - adapterConfig.setSslRequired(SslRequired.ALL.name()); - adapterConfig.setConfidentialPort(443); - } - return adapterConfig; - } - - private static ConstraintMapping constraintMapping(String pathSpec, boolean authenticate) { - var constraint = new Constraint(); - constraint.setRoles(new String[] {Constraint.ANY_AUTH}); - constraint.setAuthenticate(authenticate); - var mapping = new ConstraintMapping(); - mapping.setConstraint(constraint); - mapping.setPathSpec(pathSpec); - return mapping; - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/SecurityConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/SecurityConfig.java new file mode 100644 index 0000000000..ad39f900f7 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/SecurityConfig.java @@ -0,0 +1,39 @@ +package io.fairspace.saturn.auth; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@EnableMethodSecurity +public class SecurityConfig { + + private final JwtAuthConverter jwtAuthConverter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.authorizeHttpRequests((authorize) -> authorize + .requestMatchers(req -> req.getServletPath().contains("/actuator/health")) + .permitAll() + .requestMatchers(req -> req.getServletPath().endsWith("/favicon.ico")) + .permitAll() + .anyRequest() + .authenticated()) + .oauth2ResourceServer((oauth2) -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter))) + .sessionManagement( + (sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling( + (exceptionHandling) -> exceptionHandling.authenticationEntryPoint(new UnauthorizedEntryPoint())) + .cors(Customizer.withDefaults()); + + return http.build(); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/auth/UnauthorizedEntryPoint.java b/projects/saturn/src/main/java/io/fairspace/saturn/auth/UnauthorizedEntryPoint.java new file mode 100644 index 0000000000..cb86b9adae --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/auth/UnauthorizedEntryPoint.java @@ -0,0 +1,29 @@ +package io.fairspace.saturn.auth; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { + + public static final String LOGIN_PATH = "/login"; + + @Override + public void commence( + HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) + throws IOException { + if (request.getRequestURI().startsWith("/api/")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + if (request.getCookies() == null || request.getCookies().length == 0) { + response.addHeader("WWW-Authenticate", "Basic"); + } + } else { + response.sendRedirect(LOGIN_PATH); + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java deleted file mode 100644 index 7eb428b60e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/Config.java +++ /dev/null @@ -1,159 +0,0 @@ -package io.fairspace.saturn.config; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.NoArgsConstructor; -import org.apache.jena.atlas.json.JSON; -import org.apache.jena.tdb2.params.StoreParams; -import org.apache.jena.tdb2.params.StoreParamsCodec; - -public class Config { - static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()) - .registerModule(new SimpleModule() - .addSerializer(new StoreParamsSerializer()) - .addDeserializer(StoreParams.class, new StoreParamsDeserializer())); - - public int port = 8090; - - public int livenessPort = 8091; - - public String publicUrl = "http://localhost:8080"; - - public Jena jena = new Jena(); - - public Auth auth = new Auth(); - - public WebDAV webDAV = new WebDAV(); - - public ViewDatabase viewDatabase = new ViewDatabase(); - - public ExtraStorage extraStorage = new ExtraStorage(); - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public final Set features = new HashSet<>(); - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public Map services = new HashMap<>(); - - public Caches caches = new Caches(); - - public Search search = new Search(); - - public static class Jena { - public String metadataBaseIRI = "http://localhost/iri/"; - - public File datasetPath = new File("data/db"); - - public final StoreParams storeParams = StoreParams.getDftStoreParams(); - - public File transactionLogPath = new File("data/log"); - - public boolean bulkTransactions = true; - } - - public static class Auth { - public String authServerUrl = "http://localhost:5100/"; - public String realm = "fairspace"; - public String clientId = "workspace-client"; - public boolean enableBasicAuth; - public String superAdminUser = "organisation-admin"; - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public final Set defaultUserRoles = new HashSet<>(); - } - - public static class WebDAV { - public String blobStorePath = "data/blobs"; - } - - @Builder - @NoArgsConstructor - @AllArgsConstructor - public static class CacheConfig { - public String name; - public boolean autoRefreshEnabled = false; - public Long refreshFrequencyInHours = 240L; - } - - public static class Caches { - public CacheConfig facets = CacheConfig.builder().name("facets").build(); - public CacheConfig views = CacheConfig.builder().name("views").build(); - } - - public static class Search { - public long pageRequestTimeout = 10_000; - public long countRequestTimeout = 100_1000; - /** maxJoinItems is used to limit number of joined entries (from the join view) to decrease the response size */ - public int maxJoinItems = 50; - } - - public static class ViewDatabase { - public boolean enabled = false; - public String url = String.format("jdbc:postgresql://%s:%d/%s", "localhost", 5432, "fairspace"); - public String username = "fairspace"; - public String password = "fairspace"; - public int maxPoolSize = 50; - public long connectionTimeout = 1000; - public boolean autoCommit = false; - public boolean mvRefreshOnStartRequired = true; - } - - public static class ExtraStorage { - public String blobStorePath = "data/extra-blobs"; - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public final Set defaultRootCollections = new HashSet<>(List.of("analysis-export")); - } - - @Override - public String toString() { - try { - return MAPPER.writeValueAsString(this); - } catch (JsonProcessingException e) { - return super.toString(); - } - } - - public static class StoreParamsSerializer extends StdSerializer { - public StoreParamsSerializer() { - super(StoreParams.class); - } - - @Override - public void serialize(StoreParams value, JsonGenerator gen, SerializerProvider provider) throws IOException { - var node = MAPPER.readTree(StoreParamsCodec.encodeToJson(value).toString()); - gen.writeObject(node); - } - } - - public static class StoreParamsDeserializer extends StdDeserializer { - protected StoreParamsDeserializer() { - super(StoreParams.class); - } - - @Override - public StoreParams deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return StoreParamsCodec.decode(JSON.parse(p.readValueAsTree().toString())); - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/ConfigLoader.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/ConfigLoader.java deleted file mode 100644 index 26c4c482ab..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/ConfigLoader.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.fairspace.saturn.config; - -import java.io.File; -import java.io.IOException; - -import lombok.extern.log4j.*; - -@Log4j2 -public class ConfigLoader { - // TODO: Get rid of it. Use contexts instead - public static final Config CONFIG = loadConfig(); - public static final ViewsConfig VIEWS_CONFIG = loadViewsConfig(); - - private static Config loadConfig() { - var settingsFile = new File("application.yaml"); - if (settingsFile.exists()) { - try { - return Config.MAPPER.readValue(settingsFile, Config.class); - } catch (IOException e) { - throw new RuntimeException("Error loading configuration", e); - } - } - return new Config(); - } - - private static ViewsConfig loadViewsConfig() { - var settingsFile = new File("views.yaml"); - if (settingsFile.exists()) { - try { - return ViewsConfig.MAPPER.readValue(settingsFile, ViewsConfig.class); - } catch (IOException e) { - log.error("Error loading search configuration", e); - throw new RuntimeException("Error loading search configuration", e); - } - } - return new ViewsConfig(); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/KeycloakConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/KeycloakConfig.java new file mode 100644 index 0000000000..fdb6f55894 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/KeycloakConfig.java @@ -0,0 +1,32 @@ +package io.fairspace.saturn.config; + +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.admin.client.resource.UsersResource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.fairspace.saturn.config.properties.KeycloakClientProperties; + +@Configuration +public class KeycloakConfig { + + @Bean + public UsersResource usersResource(Keycloak keycloak, KeycloakClientProperties keycloakClientProperties) { + return keycloak.realm(keycloakClientProperties.getRealm()).users(); + } + + @Bean + public Keycloak keycloak(KeycloakClientProperties keycloakClientProperties) { + return KeycloakBuilder.builder() + .serverUrl(keycloakClientProperties.getAuthServerUrl()) + .realm(keycloakClientProperties.getRealm()) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .clientId(keycloakClientProperties.getClientId()) + .clientSecret(keycloakClientProperties.getClientSecret()) + .username(keycloakClientProperties.getClientId()) + .password(keycloakClientProperties.getClientSecret()) + .build(); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/LivenessServer.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/LivenessServer.java deleted file mode 100644 index a1ea459030..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/LivenessServer.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.fairspace.saturn.config; - -import javax.servlet.http.*; - -import lombok.extern.log4j.*; -import org.eclipse.jetty.server.*; -import org.eclipse.jetty.servlet.*; - -@Log4j2 -public class LivenessServer implements AutoCloseable { - public static class LivenessServlet extends HttpServlet { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) { - resp.setStatus(HttpServletResponse.SC_OK); - } - } - - private final Server server; - - /** - * Starts a Jetty server at port 8091 with endpoint /liveness that - * always returns OK. - */ - public LivenessServer() { - log.info("Start liveness endpoint"); - - server = new Server(ConfigLoader.CONFIG.livenessPort); - var context = new ServletContextHandler(); - context.addServlet(LivenessServer.LivenessServlet.class, "/liveness"); - server.setHandler(context); - try { - server.start(); - } catch (Exception e) { - throw new RuntimeException("Could not start liveness endpoint", e); - } - log.info("Liveness endpoint has started"); - } - - @Override - public void close() throws Exception { - log.info("Closing liveness endpoint"); - server.stop(); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/MappingConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/MappingConfig.java new file mode 100644 index 0000000000..b7b94deb68 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/MappingConfig.java @@ -0,0 +1,35 @@ +package io.fairspace.saturn.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import io.fairspace.saturn.services.IRIModule; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; + +@Configuration +public class MappingConfig { + + /** + * The maapper is used to read views.yaml configuration file + */ + @Bean + public ObjectMapper yamlObjectMapper() { + return new ObjectMapper(new YAMLFactory()); + } + + @Bean + @Primary // to be used by default, including serialization of JSON responses to HTTP requests + public ObjectMapper jsonObjectMapper() { + return new ObjectMapper() + .registerModule(new IRIModule()) + .registerModule(new JavaTimeModule()) + .configure(WRITE_DATES_AS_TIMESTAMPS, false) + .configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/MetadataConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/MetadataConfig.java new file mode 100644 index 0000000000..0aaddc8822 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/MetadataConfig.java @@ -0,0 +1,38 @@ +package io.fairspace.saturn.config; + +import org.apache.jena.query.Dataset; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.sparql.util.Symbol; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.metadata.MetadataPermissions; +import io.fairspace.saturn.services.metadata.MetadataService; +import io.fairspace.saturn.services.metadata.validation.ComposedValidator; + +@Configuration +public class MetadataConfig { + + public static final Symbol METADATA_SERVICE = Symbol.create("metadata_service"); + + @Bean + public MetadataService metadataService( + Transactions transactions, + MetadataPermissions metadataPermissions, + @Qualifier("dataset") Dataset dataset, + @Qualifier("vocabulary") Model vocabulary, + @Qualifier("systemVocabulary") Model systemVocabulary, + @Qualifier("composedValidator") ComposedValidator composedValidator) { + var metadataService = + new MetadataService(transactions, vocabulary, systemVocabulary, composedValidator, metadataPermissions); + // This is a workaround (old, not a new one) to resolve circular dependency: + // MetadataService --> ComposedValidator --> URIPrefixValidator --> DavFactory --> DirectoryResource --> + // MetadataService + // TODO: refactor to avoid circular dependency and !!!USE!!! injection using Spring (at least in DavFactory) + // this to-do supposes getting rid of the following line and using dataset context for storing metadataService + dataset.getContext().set(METADATA_SERVICE, metadataService); + return metadataService; + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/Protected_SPARQL_QueryDataset.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/Protected_SPARQL_QueryDataset.java deleted file mode 100644 index 590aa5fd51..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/Protected_SPARQL_QueryDataset.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.fairspace.saturn.config; - -import lombok.*; -import lombok.extern.slf4j.*; -import org.apache.jena.fuseki.servlets.*; - -import io.fairspace.saturn.services.users.*; - -import static org.apache.jena.fuseki.servlets.ServletOps.errorForbidden; - -@Slf4j -public class Protected_SPARQL_QueryDataset extends SPARQL_QueryDataset { - private final UserService userService; - - public Protected_SPARQL_QueryDataset(UserService userService) { - this.userService = userService; - } - - @SneakyThrows - @Override - protected void validateRequest(HttpAction action) { - var user = userService.currentUser(); - if (user == null || !user.isCanQueryMetadata()) { - log.error( - "The current user has no metadata querying role: {}", - userService.currentUser().getName()); - errorForbidden("The current user has no metadata querying role"); - } else { - super.validateRequest(action); - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfModelConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfModelConfig.java new file mode 100644 index 0000000000..2fa942cd27 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfModelConfig.java @@ -0,0 +1,28 @@ +package io.fairspace.saturn.config; + +import org.apache.jena.rdf.model.Model; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.apache.jena.riot.RDFDataMgr.loadModel; + +@Configuration +public class RdfModelConfig { + + @Bean + public Model systemVocabulary() { + return loadModel("system-vocabulary.ttl"); + } + + @Bean + public Model userVocabulary() { + return loadModel("vocabulary.ttl"); + } + + @Bean + public Model vocabulary( + @Qualifier("systemVocabulary") Model systemVocabulary, @Qualifier("userVocabulary") Model userVocabulary) { + return systemVocabulary.union(userVocabulary); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfStorageConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfStorageConfig.java new file mode 100644 index 0000000000..3d4d2ffd95 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/RdfStorageConfig.java @@ -0,0 +1,36 @@ +package io.fairspace.saturn.config; + +import org.apache.jena.query.Dataset; +import org.apache.jena.sparql.core.DatasetImpl; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.rdf.SaturnDatasetFactory; +import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; +import io.fairspace.saturn.services.metadata.MetadataPermissions; +import io.fairspace.saturn.services.views.ViewStoreClientFactory; + +@Configuration +public class RdfStorageConfig { + + @Value("${application.publicUrl}") + private String publicUrl; + + @Bean + public Dataset dataset( + ViewsProperties viewsProperties, + JenaProperties jenaProperties, + @Nullable ViewStoreClientFactory viewStoreClientFactory) { + return SaturnDatasetFactory.connect(viewsProperties, jenaProperties, viewStoreClientFactory, publicUrl); + } + + @Bean + public Dataset filteredDataset(Dataset dataset, MetadataPermissions metadataPermissions) { + var filteredDatasetGraph = new FilteredDatasetGraph(dataset.asDatasetGraph(), metadataPermissions); + return DatasetImpl.wrap(filteredDatasetGraph); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java deleted file mode 100644 index b426a670ed..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SaturnSparkFilter.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.fairspace.saturn.config; - -import javax.servlet.FilterConfig; - -import spark.servlet.SparkApplication; -import spark.servlet.SparkFilter; - -public class SaturnSparkFilter extends SparkFilter { - private final SparkApplication[] apps; - - public SaturnSparkFilter(SparkApplication... apps) { - this.apps = apps; - } - - @Override - protected SparkApplication[] getApplications(FilterConfig filterConfig) { - return apps; - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SecondaryStorageConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SecondaryStorageConfig.java new file mode 100644 index 0000000000..677567f432 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/SecondaryStorageConfig.java @@ -0,0 +1,33 @@ +package io.fairspace.saturn.config; + +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.fairspace.saturn.config.properties.ViewDatabaseProperties; + +@Configuration +@ConditionalOnProperty(value = "application.view-database.enabled", havingValue = "true") +public class SecondaryStorageConfig { + + @Bean + public DataSource dataSource(ViewDatabaseProperties viewDatabaseProperties) { + var databaseConfig = getHikariConfig(viewDatabaseProperties); + return new HikariDataSource(databaseConfig); + } + + private HikariConfig getHikariConfig(ViewDatabaseProperties viewDatabaseProperties) { + var databaseConfig = new HikariConfig(); + databaseConfig.setJdbcUrl(viewDatabaseProperties.getUrl()); + databaseConfig.setUsername(viewDatabaseProperties.getUsername()); + databaseConfig.setPassword(viewDatabaseProperties.getPassword()); + databaseConfig.setAutoCommit(viewDatabaseProperties.isAutoCommitEnabled()); + databaseConfig.setConnectionTimeout(viewDatabaseProperties.getConnectionTimeout()); + databaseConfig.setMaximumPoolSize(viewDatabaseProperties.getMaxPoolSize()); + return databaseConfig; + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java new file mode 100644 index 0000000000..054671882a --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/ServiceConfig.java @@ -0,0 +1,56 @@ +package io.fairspace.saturn.config; + +import org.apache.jena.query.Dataset; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; + +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.search.FileSearchService; +import io.fairspace.saturn.services.search.JdbcFileSearchService; +import io.fairspace.saturn.services.search.SparqlFileSearchService; +import io.fairspace.saturn.services.views.JdbcQueryService; +import io.fairspace.saturn.services.views.QueryService; +import io.fairspace.saturn.services.views.SparqlQueryService; +import io.fairspace.saturn.services.views.ViewStoreClientFactory; +import io.fairspace.saturn.services.views.ViewStoreReader; +import io.fairspace.saturn.webdav.DavFactory; + +import static io.fairspace.saturn.services.views.ViewStoreClientFactory.protectedResources; + +@Configuration +public class ServiceConfig { + + @Bean + public QueryService queryService( + SparqlQueryService sparqlQueryService, + @Nullable ViewStoreClientFactory viewStoreClientFactory, + Transactions transactions, + @Qualifier("davFactory") DavFactory davFactory, + ViewStoreReader viewStoreReader) { + return viewStoreClientFactory == null + ? sparqlQueryService + : new JdbcQueryService(transactions, davFactory.root, viewStoreReader); + } + + @Bean + public FileSearchService fileSearchService( + @Qualifier("filteredDataset") Dataset filteredDataset, + @Nullable ViewStoreClientFactory viewStoreClientFactory, + ViewsProperties viewsProperties, + Transactions transactions, + @Qualifier("davFactory") DavFactory davFactory, + ViewStoreReader viewStoreReader) { + // File search should be done using JDBC for performance reasons. However, if the view store is not available, + // or collections and files view is not configured, we fall back to using SPARQL queries on the RDF database + // directly. + boolean useSparqlFileSearchService = viewStoreClientFactory == null + || viewsProperties.views.stream().noneMatch(view -> protectedResources.containsAll(view.types)); + + return useSparqlFileSearchService + ? new SparqlFileSearchService(filteredDataset) + : new JdbcFileSearchService(transactions, davFactory.root, viewStoreReader); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/Services.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/Services.java deleted file mode 100644 index 4ae86d3797..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/Services.java +++ /dev/null @@ -1,157 +0,0 @@ -package io.fairspace.saturn.config; - -import java.io.File; -import javax.servlet.http.HttpServlet; - -import io.milton.resource.Resource; -import lombok.Getter; -import lombok.NonNull; -import lombok.extern.log4j.*; -import org.apache.jena.query.Dataset; -import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.sparql.core.DatasetImpl; -import org.apache.jena.sparql.util.Symbol; - -import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; -import io.fairspace.saturn.rdf.transactions.BulkTransactions; -import io.fairspace.saturn.rdf.transactions.SimpleTransactions; -import io.fairspace.saturn.rdf.transactions.Transactions; -import io.fairspace.saturn.services.health.HealthService; -import io.fairspace.saturn.services.maintenance.MaintenanceService; -import io.fairspace.saturn.services.metadata.MetadataPermissions; -import io.fairspace.saturn.services.metadata.MetadataService; -import io.fairspace.saturn.services.metadata.validation.*; -import io.fairspace.saturn.services.search.FileSearchService; -import io.fairspace.saturn.services.search.JdbcFileSearchService; -import io.fairspace.saturn.services.search.SearchService; -import io.fairspace.saturn.services.search.SparqlFileSearchService; -import io.fairspace.saturn.services.users.UserService; -import io.fairspace.saturn.services.views.*; -import io.fairspace.saturn.services.workspaces.WorkspaceService; -import io.fairspace.saturn.webdav.*; -import io.fairspace.saturn.webdav.blobstore.BlobStore; -import io.fairspace.saturn.webdav.blobstore.DeletableLocalBlobStore; -import io.fairspace.saturn.webdav.blobstore.LocalBlobStore; - -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; -import static io.fairspace.saturn.services.views.ViewStoreClientFactory.protectedResources; -import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; - -@Log4j2 -@Getter -public class Services { - public static final Symbol METADATA_SERVICE = Symbol.create("metadata_service"); - - private final Config config; - private final Transactions transactions; - - private final WorkspaceService workspaceService; - private final UserService userService; - private final MetadataPermissions metadataPermissions; - private final MetadataService metadataService; - private final ViewService viewService; - private final QueryService queryService; - private final SearchService searchService; - private final FileSearchService fileSearchService; - private final BlobStore blobStore; - private final DavFactory davFactory; - private final HttpServlet davServlet; - - private final BlobStore extraBlobStore; - private final DavFactory extraDavFactory; - private final HttpServlet extraDavServlet; - private final DatasetGraph filteredDatasetGraph; - private final HealthService healthService; - private final MaintenanceService maintenanceService; - - public Services( - @NonNull Config config, - @NonNull ViewsConfig viewsConfig, - @NonNull Dataset dataset, - ViewStoreClientFactory viewStoreClientFactory) { - this.config = config; - this.transactions = - config.jena.bulkTransactions ? new BulkTransactions(dataset) : new SimpleTransactions(dataset); - - userService = new UserService(config.auth, transactions); - - blobStore = new LocalBlobStore(new File(config.webDAV.blobStorePath)); - davFactory = new DavFactory( - dataset.getDefaultModel().createResource(config.publicUrl + "/api/webdav"), - blobStore, - userService, - dataset.getContext()); - davServlet = new WebDAVServlet(davFactory, transactions, blobStore); - - if (CONFIG.features.contains(Feature.ExtraStorage)) { - extraBlobStore = new DeletableLocalBlobStore(new File(config.extraStorage.blobStorePath)); - extraDavFactory = new DavFactory( - dataset.getDefaultModel().createResource(config.publicUrl + "/api/extra-storage"), - extraBlobStore, - userService, - dataset.getContext()); - extraDavServlet = new WebDAVServlet(extraDavFactory, transactions, extraBlobStore); - initExtraStorageRootDirectories(); - } else { - extraBlobStore = null; - extraDavFactory = null; - extraDavServlet = null; - } - - workspaceService = new WorkspaceService(transactions, userService); - - metadataPermissions = new MetadataPermissions(workspaceService, davFactory, userService); - - var metadataValidator = new ComposedValidator( - new MachineOnlyClassesValidator(VOCABULARY), - new ProtectMachineOnlyPredicatesValidator(VOCABULARY), - new URIPrefixValidator(((Resource) davFactory.root).getUniqueId()), - new DeletionValidator(), - new UniqueLabelValidator(), - new ShaclValidator(VOCABULARY)); - - metadataService = new MetadataService(transactions, VOCABULARY, metadataValidator, metadataPermissions); - dataset.getContext().set(METADATA_SERVICE, metadataService); - - filteredDatasetGraph = new FilteredDatasetGraph(dataset.asDatasetGraph(), metadataPermissions); - var filteredDataset = DatasetImpl.wrap(filteredDatasetGraph); - - queryService = viewStoreClientFactory == null - ? new SparqlQueryService(config.search, viewsConfig, filteredDataset) - : new JdbcQueryService( - config.search, viewsConfig, viewStoreClientFactory, transactions, davFactory.root); - - // File search should be done using JDBC for performance reasons. However, if the view store is not available, - // or collections and files view is not configured, we fall back to using SPARQL queries on the RDF database - // directly. - boolean useSparqlFileSearchService = viewStoreClientFactory == null - || viewsConfig.views.stream().noneMatch(view -> protectedResources.containsAll(view.types)); - - fileSearchService = useSparqlFileSearchService - ? new SparqlFileSearchService(filteredDataset) - : new JdbcFileSearchService( - config.search, viewsConfig, viewStoreClientFactory, transactions, davFactory.root); - - viewService = - new ViewService(config, viewsConfig, filteredDataset, viewStoreClientFactory, metadataPermissions); - - maintenanceService = new MaintenanceService(userService, dataset, viewStoreClientFactory, viewService); - - searchService = new SearchService(filteredDataset); - - healthService = new HealthService(viewStoreClientFactory == null ? null : viewStoreClientFactory.dataSource); - } - - private void initExtraStorageRootDirectories() { - this.transactions.calculateWrite(ds2 -> { - for (var rc : CONFIG.extraStorage.defaultRootCollections) { - try { - extraDavFactory.root.createCollection(rc); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - return null; - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java deleted file mode 100644 index a8cd0b75cb..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/SparkFilterFactory.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.fairspace.saturn.config; - -import javax.servlet.Filter; - -import io.fairspace.saturn.services.features.FeaturesApp; -import io.fairspace.saturn.services.health.HealthApp; -import io.fairspace.saturn.services.maintenance.MaintenanceApp; -import io.fairspace.saturn.services.metadata.MetadataApp; -import io.fairspace.saturn.services.metadata.VocabularyApp; -import io.fairspace.saturn.services.search.SearchApp; -import io.fairspace.saturn.services.users.*; -import io.fairspace.saturn.services.views.ViewApp; -import io.fairspace.saturn.services.workspaces.WorkspaceApp; - -public class SparkFilterFactory { - public static Filter createSparkFilter(String apiPathPrefix, Services svc, Config config) { - return new SaturnSparkFilter( - new WorkspaceApp(apiPathPrefix + "/workspaces", svc.getWorkspaceService()), - new MetadataApp(apiPathPrefix + "/metadata", svc.getMetadataService()), - new ViewApp(apiPathPrefix + "/views", svc.getViewService(), svc.getQueryService()), - new SearchApp(apiPathPrefix + "/search", svc.getSearchService(), svc.getFileSearchService()), - new VocabularyApp(apiPathPrefix + "/vocabulary"), - new UserApp(apiPathPrefix + "/users", svc.getUserService()), - new FeaturesApp(apiPathPrefix + "/features", config.features), - new HealthApp(apiPathPrefix + "/health", svc.getHealthService()), - new MaintenanceApp(apiPathPrefix + "/maintenance", svc.getMaintenanceService()), - new LogoutApp("/logout", svc.getUserService(), config)); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/ViewsConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/ViewsConfig.java index 6a9a1dad47..9a89762507 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/ViewsConfig.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/ViewsConfig.java @@ -1,141 +1,43 @@ package io.fairspace.saturn.config; -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.io.IOException; -import com.fasterxml.jackson.annotation.*; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.dataformat.yaml.*; -import jakarta.validation.constraints.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ResourceUtils; -public class ViewsConfig { - - private Map nameToViewConfigMap; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.services.views.ViewStoreClient; - public static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public List views = new ArrayList<>(); +@Slf4j +@Configuration +public class ViewsConfig { - public Optional getViewConfig(String viewName) { - if (nameToViewConfigMap == null) { - nameToViewConfigMap = views.stream().collect(Collectors.toMap(view -> view.name, Function.identity())); - } - return Optional.ofNullable(nameToViewConfigMap.get(viewName)); + @Bean + public ViewsProperties viewsProperties(@Qualifier("yamlObjectMapper") ObjectMapper yamlObjectMapper) { + var viewsProperties = loadViewsConfig(yamlObjectMapper); + viewsProperties.init(); + return viewsProperties; } - public enum ColumnType { - Text, - Set, - Term, - TermSet, - Number, - Date, - Boolean, - Identifier; - - public boolean isSet() { - return this == Set || this == TermSet; - } - - @JsonValue - public String getName() { - return this.name(); - } - - private static final Map mapping = new HashMap<>(); - - static { - for (ColumnType type : values()) { - mapping.put(type.name().toLowerCase(), type); - } - } - - @JsonCreator - public static ColumnType forName(String name) { - if (name == null) { - return null; - } - name = name.toLowerCase(); - if (!mapping.containsKey(name)) { - throw new IllegalArgumentException("Unknown column type: " + name); - } - return mapping.get(name); - } + @Bean + ViewStoreClient.ViewStoreConfiguration viewStoreConfiguration(ViewsProperties viewsProperties) { + return new ViewStoreClient.ViewStoreConfiguration(viewsProperties); } - public static class View { - /** - * The view name. - */ - @NotBlank - public String name; - /** - * The view title. - */ - @NotBlank - public String title; - /** - * The name of the items that appear as rows. - */ - public String itemName; - /** - * The max count value to be requested for a view total count. - * If total count is greater than this max value, the total count value will look like 'more than 1000' on FE. - * This is to prevent performance issues when the total count is too large. - */ - public Long maxDisplayCount; - /** - * The URLs of the types of entities that should be indexed in this view. - */ - @JsonSetter(nulls = Nulls.AS_EMPTY) - public List types; - /** - * Specifies which other views (and which columns) to embed in this view. - */ - @JsonSetter(nulls = Nulls.AS_EMPTY) - public List join; - /** - * The columns of the view, not including columns from joined views. - */ - @JsonSetter(nulls = Nulls.AS_EMPTY) - public List columns; - - public static class Column { - @NotBlank - public String name; - - @NotBlank - public String title; - - @NotNull - public ColumnType type; - - @NotBlank - public String source; - // displayIndex determines the order of columns on the view page. - @NotNull - public Integer displayIndex = Integer.MAX_VALUE; - - public String rdfType; - public int priority; - } - - public static class JoinView { - @NotBlank - public String view; - - @NotBlank - public String on; - - public boolean reverse = false; - - @JsonSetter(nulls = Nulls.AS_EMPTY) - public List include; - // displayIndex determines the order of columns on the view page, for joinView it is the column displaying - // the related entity - public Integer displayIndex = Integer.MAX_VALUE; + private static ViewsProperties loadViewsConfig(@Qualifier("yamlObjectMapper") ObjectMapper objectMapper) { + try { + var settingsFile = ResourceUtils.getFile("classpath:views.yaml"); + if (settingsFile.exists()) { + return objectMapper.readValue(settingsFile, ViewsProperties.class); + } + } catch (IOException e) { + log.error("Error loading search configuration", e); + throw new RuntimeException("Error loading search configuration", e); } + return new ViewsProperties(); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVConfig.java new file mode 100644 index 0000000000..0fac2747a8 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVConfig.java @@ -0,0 +1,99 @@ +package io.fairspace.saturn.config; + +import java.io.File; +import java.util.Arrays; + +import org.apache.jena.query.Dataset; +import org.apache.jena.rdf.model.Model; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.web.firewall.StrictHttpFirewall; + +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.users.UserService; +import io.fairspace.saturn.webdav.DavFactory; +import io.fairspace.saturn.webdav.WebDAVServlet; +import io.fairspace.saturn.webdav.blobstore.BlobStore; +import io.fairspace.saturn.webdav.blobstore.LocalBlobStore; + +@Configuration +public class WebDAVConfig { + + public static final String WEB_DAV_URL_PATH = "/api/webdav"; + + @Value("${application.publicUrl}") + private String publicUrl; + + @Bean + public ServletRegistrationBean webDavServletRegistrationBean( + @Qualifier("webDavServlet") WebDAVServlet webDavServlet) { + return new ServletRegistrationBean<>(webDavServlet, "/webdav/*"); + } + + @Bean + public BlobStore blobStore(WebDavProperties webDavProperties) { + return new LocalBlobStore(new File(webDavProperties.getBlobStorePath())); + } + + @Bean + public DavFactory davFactory( + @Qualifier("dataset") Dataset dataset, + @Qualifier("blobStore") BlobStore blobStore, + UserService userService, + WebDavProperties webDavProperties, + @Qualifier("userVocabulary") Model userVocabulary, + @Qualifier("vocabulary") Model vocabulary) { + return new DavFactory( + dataset.getDefaultModel().createResource(publicUrl + WEB_DAV_URL_PATH), + blobStore, + userService, + dataset.getContext(), + webDavProperties, + userVocabulary, + vocabulary); + } + + @Bean + public WebDAVServlet webDavServlet( + @Qualifier("davFactory") DavFactory davFactory, + Transactions transactions, + @Qualifier("blobStore") BlobStore blobStore) { + return new WebDAVServlet(davFactory, transactions, blobStore); + } + + /** + * Configure the firewall to allow WebDAV methods. + * By default, Spring Security blocks all methods except GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, and TRACE. + * + * @return the firewall + */ + @Bean + public StrictHttpFirewall webDavFilterFirewall() { + final StrictHttpFirewall firewall = new StrictHttpFirewall(); + firewall.setAllowedHttpMethods( + Arrays.stream(ExtendedHttpMethod.values()).map(Enum::name).toList()); + return firewall; + } + + private enum ExtendedHttpMethod { + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE, + COPY, + LOCK, + MKCOL, + MOVE, + PROPFIND, + PROPPATCH, + UNLOCK + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVExtraStorageConfig.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVExtraStorageConfig.java new file mode 100644 index 0000000000..94d2d5b180 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/WebDAVExtraStorageConfig.java @@ -0,0 +1,85 @@ +package io.fairspace.saturn.config; + +import java.io.File; + +import org.apache.jena.query.Dataset; +import org.apache.jena.rdf.model.Model; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.fairspace.saturn.config.condition.ConditionalOnMultiValuedProperty; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.users.UserService; +import io.fairspace.saturn.webdav.DavFactory; +import io.fairspace.saturn.webdav.WebDAVServlet; +import io.fairspace.saturn.webdav.blobstore.BlobStore; +import io.fairspace.saturn.webdav.blobstore.DeletableLocalBlobStore; + +@Configuration +@ConditionalOnMultiValuedProperty(prefix = "application", name = "features", havingValue = "ExtraStorage") +public class WebDAVExtraStorageConfig { + + public static final String WEB_DAV_EXTRA_URL_PATH = "/api/extra-storage"; + + @Value("${application.publicUrl}") + private String publicUrl; + + @Bean + public ServletRegistrationBean extraWebDavServletRegistrationBean( + @Qualifier("extraDavServlet") WebDAVServlet extraDavServlet) { + return new ServletRegistrationBean<>(extraDavServlet, "/extra-storage/*"); + } + + @Bean + public BlobStore extraBlobStore(WebDavProperties webDavProperties) { + return new DeletableLocalBlobStore( + new File(webDavProperties.getExtraStorage().getBlobStorePath())); + } + + @Bean + public DavFactory extraDavFactory( + @Qualifier("dataset") Dataset dataset, + @Qualifier("extraBlobStore") BlobStore extraBlobStore, + UserService userService, + Transactions transactions, + WebDavProperties webDavProperties, + @Qualifier("userVocabulary") Model userVocabulary, + @Qualifier("vocabulary") Model vocabulary) { + var extraDavFactory = new DavFactory( + dataset.getDefaultModel().createResource(publicUrl + WEB_DAV_EXTRA_URL_PATH), + extraBlobStore, + userService, + dataset.getContext(), + webDavProperties, + userVocabulary, + vocabulary); + initExtraStorageRootDirectories(webDavProperties.getExtraStorage(), extraDavFactory, transactions); + return extraDavFactory; + } + + @Bean + public WebDAVServlet extraDavServlet( + @Qualifier("extraDavFactory") DavFactory davFactory, + Transactions transactions, + @Qualifier("extraBlobStore") BlobStore blobStore) { + return new WebDAVServlet(davFactory, transactions, blobStore); + } + + private void initExtraStorageRootDirectories( + WebDavProperties.ExtraStorage extraStorage, DavFactory extraDavFactory, Transactions transactions) { + transactions.calculateWrite(ds2 -> { + for (var rc : extraStorage.getDefaultRootCollections()) { + try { + extraDavFactory.root.createCollection(rc); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + }); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/ConditionalOnMultiValuedProperty.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/ConditionalOnMultiValuedProperty.java new file mode 100644 index 0000000000..8ece33b146 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/ConditionalOnMultiValuedProperty.java @@ -0,0 +1,21 @@ +package io.fairspace.saturn.config.condition; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +@Documented +@Conditional(OnMultiValuedPropertyCondition.class) +public @interface ConditionalOnMultiValuedProperty { + String prefix(); + + String name(); + + String havingValue(); +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/OnMultiValuedPropertyCondition.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/OnMultiValuedPropertyCondition.java new file mode 100644 index 0000000000..f9e818faf0 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/condition/OnMultiValuedPropertyCondition.java @@ -0,0 +1,32 @@ +package io.fairspace.saturn.config.condition; + +import java.util.ArrayList; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class OnMultiValuedPropertyCondition extends SpringBootCondition { + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + MergedAnnotation conditionalMultiValuedPropertyAnnotation = + metadata.getAnnotations().get(ConditionalOnMultiValuedProperty.class.getName()); + var prefix = conditionalMultiValuedPropertyAnnotation.getString("prefix"); + var name = conditionalMultiValuedPropertyAnnotation.getString("name"); + + var values = new ArrayList<>(); + Binder.get(context.getEnvironment()).bind(prefix + "." + name, Bindable.ofInstance(values)); + + String valueToCheck = conditionalMultiValuedPropertyAnnotation.getString("havingValue"); + boolean valueExists = values.stream().anyMatch(valueToCheck::equals); + if (valueExists) { + return ConditionOutcome.match(); + } else { + return ConditionOutcome.noMatch("define your message!"); + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/Feature.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/enums/Feature.java similarity index 75% rename from projects/saturn/src/main/java/io/fairspace/saturn/config/Feature.java rename to projects/saturn/src/main/java/io/fairspace/saturn/config/enums/Feature.java index 065e62bb94..ad8cc010b3 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/config/Feature.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/enums/Feature.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.config; +package io.fairspace.saturn.config.enums; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/health/FairspaceReadinessStateHealthIndicator.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/health/FairspaceReadinessStateHealthIndicator.java new file mode 100644 index 0000000000..670bac4740 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/health/FairspaceReadinessStateHealthIndicator.java @@ -0,0 +1,41 @@ +package io.fairspace.saturn.config.health; + +import org.springframework.boot.actuate.availability.ReadinessStateHealthIndicator; +import org.springframework.boot.availability.ApplicationAvailability; +import org.springframework.boot.availability.AvailabilityState; +import org.springframework.boot.availability.ReadinessState; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +import io.fairspace.saturn.services.views.ViewStoreClientFactory; + +@Component +public class FairspaceReadinessStateHealthIndicator extends ReadinessStateHealthIndicator { + + private static final int CONNECTION_TIMEOUT = 1; + + private final ViewStoreClientFactory viewStoreClientFactory; + + public FairspaceReadinessStateHealthIndicator( + ApplicationAvailability availability, @Nullable ViewStoreClientFactory viewStoreClientFactory) { + super(availability); + this.viewStoreClientFactory = viewStoreClientFactory; + } + + @Override + protected AvailabilityState getState(ApplicationAvailability applicationAvailability) { + return isConnectionValid() ? ReadinessState.ACCEPTING_TRAFFIC : ReadinessState.REFUSING_TRAFFIC; + } + + private boolean isConnectionValid() { + if (viewStoreClientFactory != null) { + try (var connection = viewStoreClientFactory.dataSource.getConnection()) { + return connection.isValid(CONNECTION_TIMEOUT); + } catch (Exception e) { + return false; + } + } else { + return true; + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/CacheProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/CacheProperties.java new file mode 100644 index 0000000000..6cf642be0f --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/CacheProperties.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.cache") +public class CacheProperties { + + private Cache facets = new Cache(); + + private Cache views = new Cache(); + + @Data + public static class Cache { + + private String name; + + private boolean autoRefreshEnabled; + + private Long refreshFrequencyInHours; + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/FeatureProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/FeatureProperties.java new file mode 100644 index 0000000000..8b139b590f --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/FeatureProperties.java @@ -0,0 +1,17 @@ +package io.fairspace.saturn.config.properties; + +import java.util.Set; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import io.fairspace.saturn.config.enums.Feature; + +@Data +@Component +@ConfigurationProperties(prefix = "application") +public class FeatureProperties { + + private Set features; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/JenaProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/JenaProperties.java new file mode 100644 index 0000000000..1a21dc3039 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/JenaProperties.java @@ -0,0 +1,48 @@ +package io.fairspace.saturn.config.properties; + +import java.io.File; + +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import org.apache.jena.tdb2.params.StoreParams; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.jena") +public class JenaProperties { + + // TODO: this is a terrible idea to make it static, it's a quick fix for SparqlUtils & RequestContext + private static String metadataBaseIRI; + + private File datasetPath; + + private File transactionLogPath; + + private boolean bulkTransactions; + + private long sparqlQueryTimeout; + + private final StoreParams storeParams; + + public static String getMetadataBaseIri() { + return JenaProperties.metadataBaseIRI; + } + + @VisibleForTesting + public static void setMetadataBaseIRI(String metadataBaseIRI) { + JenaProperties.metadataBaseIRI = metadataBaseIRI; + } + + // Not a common practice, but a way to make the value available in a static context + @Autowired + public JenaProperties( + @Value("${application.jena.metadataBaseIRI}") String metadataBaseIRI, + StoreParamsProperties storeParamsProperties) { + JenaProperties.metadataBaseIRI = metadataBaseIRI; + this.storeParams = storeParamsProperties.buildStoreParams(); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/KeycloakClientProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/KeycloakClientProperties.java new file mode 100644 index 0000000000..7170b6230e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/KeycloakClientProperties.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.config.properties; + +import java.util.List; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "keycloak") +public class KeycloakClientProperties { + + private String authServerUrl; + + private String realm; + + private String clientId; + + private String clientSecret; + + private String superAdminUser; + + private List defaultUserRoles; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/SearchProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/SearchProperties.java new file mode 100644 index 0000000000..a3a432428c --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/SearchProperties.java @@ -0,0 +1,20 @@ +package io.fairspace.saturn.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.search") +public class SearchProperties { + + private int pageRequestTimeout; + + private int countRequestTimeout; + + /** + * maxJoinItems is used to limit number of joined entries (from the join view) to decrease the response size + */ + private int maxJoinItems; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/StoreParamsProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/StoreParamsProperties.java new file mode 100644 index 0000000000..125f0fd41e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/StoreParamsProperties.java @@ -0,0 +1,74 @@ +package io.fairspace.saturn.config.properties; + +import java.util.List; + +import lombok.Data; +import org.apache.jena.dboe.base.block.FileMode; +import org.apache.jena.tdb2.params.StoreParams; +import org.apache.jena.tdb2.params.StoreParamsBuilder; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.jena.store-params") +public class StoreParamsProperties { + + private FileMode fileMode; + private Integer blockSize; + private Integer blockReadCacheSize; + private Integer blockWriteCacheSize; + private Integer node2nodeidCacheSize; + private Integer nodeid2nodeCacheSize; + private Integer nodeMissCacheSize; + private String nodeTable; + private String tripleIndexPrimary; + private List tripleIndexes; + private String quadIndexPrimary; + private List quadIndexes; + private String prefixTable; + private String prefixIndexPrimary; + private List prefixIndexes; + + public StoreParams buildStoreParams() { + return isStoreParamsSet() ? createStoreParams() : StoreParams.getDftStoreParams(); + } + + private StoreParams createStoreParams() { + return StoreParamsBuilder.create() + .fileMode(fileMode) + .blockSize(blockSize) + .blockReadCacheSize(blockReadCacheSize) + .blockWriteCacheSize(blockWriteCacheSize) + .node2NodeIdCacheSize(node2nodeidCacheSize) + .nodeId2NodeCacheSize(nodeid2nodeCacheSize) + .nodeMissCacheSize(nodeMissCacheSize) + .nodeTableBaseName(nodeTable) + .primaryIndexTriples(tripleIndexPrimary) + .tripleIndexes(tripleIndexes.toArray(new String[0])) + .primaryIndexQuads(quadIndexPrimary) + .quadIndexes(quadIndexes.toArray(new String[0])) + .prefixTableBaseName(prefixTable) + .primaryIndexPrefix(prefixIndexPrimary) + .prefixIndexes(prefixIndexes.toArray(new String[0])) + .build(); + } + + private boolean isStoreParamsSet() { + return fileMode != null + || blockSize != null + || blockReadCacheSize != null + || blockWriteCacheSize != null + || node2nodeidCacheSize != null + || nodeid2nodeCacheSize != null + || nodeMissCacheSize != null + || nodeTable != null + || tripleIndexPrimary != null + || tripleIndexes != null + || quadIndexPrimary != null + || quadIndexes != null + || prefixTable != null + || prefixIndexPrimary != null + || prefixIndexes != null; + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewDatabaseProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewDatabaseProperties.java new file mode 100644 index 0000000000..3e8ddc1dd3 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewDatabaseProperties.java @@ -0,0 +1,20 @@ +package io.fairspace.saturn.config.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.view-database") +public class ViewDatabaseProperties { + + private boolean enabled; + private String url; + private String username; + private String password; + private boolean autoCommitEnabled; + private int maxPoolSize; + private int connectionTimeout; + private boolean mvRefreshOnStartRequired; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewsProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewsProperties.java new file mode 100644 index 0000000000..8e1b456d23 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/ViewsProperties.java @@ -0,0 +1,147 @@ +package io.fairspace.saturn.config.properties; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.Nulls; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class ViewsProperties { + + @JsonSetter(nulls = Nulls.AS_EMPTY) + public List views = new ArrayList<>(); + + private Map nameToViewConfigMap; + + public Optional getViewConfig(String viewName) { + return Optional.ofNullable(nameToViewConfigMap.get(viewName)); + } + + public void init() { + nameToViewConfigMap = views.stream().collect(Collectors.toMap(view -> view.name, Function.identity())); + } + + public enum ColumnType { + Text, + Set, + Term, + TermSet, + Number, + Date, + Boolean, + Identifier; + + public boolean isSet() { + return this == Set || this == TermSet; + } + + @JsonValue + public String getName() { + return this.name(); + } + + private static final Map mapping = new HashMap<>(); + + static { + for (ColumnType type : values()) { + mapping.put(type.name().toLowerCase(), type); + } + } + + @JsonCreator + public static ColumnType forName(String name) { + if (name == null) { + return null; + } + name = name.toLowerCase(); + if (!mapping.containsKey(name)) { + throw new IllegalArgumentException("Unknown column type: " + name); + } + return mapping.get(name); + } + } + + public static class View { + /** + * The view name. + */ + @NotBlank + public String name; + /** + * The view title. + */ + @NotBlank + public String title; + /** + * The name of the items that appear as rows. + */ + public String itemName; + /** + * The max count value to be requested for a view total count. + * If total count is greater than this max value, the total count value will look like 'more than 1000' on FE. + * This is to prevent performance issues when the total count is too large. + */ + public Long maxDisplayCount; + /** + * The URLs of the types of entities that should be indexed in this view. + */ + @JsonSetter(nulls = Nulls.AS_EMPTY) + public List types; + /** + * Specifies which other views (and which columns) to embed in this view. + */ + @JsonSetter(nulls = Nulls.AS_EMPTY) + public List join; + /** + * The columns of the view, not including columns from joined views. + */ + @JsonSetter(nulls = Nulls.AS_EMPTY) + public List columns; + + public static class Column { + @NotBlank + public String name; + + @NotBlank + public String title; + + @NotNull + public ColumnType type; + + @NotBlank + public String source; + // displayIndex determines the order of columns on the view page. + @NotNull + public Integer displayIndex = Integer.MAX_VALUE; + + public String rdfType; + + public int priority; + } + + public static class JoinView { + @NotBlank + public String view; + + @NotBlank + public String on; + + public boolean reverse = false; + + @JsonSetter(nulls = Nulls.AS_EMPTY) + public List include; + // displayIndex determines the order of columns on the view page, for joinView it is the column displaying + // the related entity + public Integer displayIndex = Integer.MAX_VALUE; + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/WebDavProperties.java b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/WebDavProperties.java new file mode 100644 index 0000000000..653243a2a7 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/config/properties/WebDavProperties.java @@ -0,0 +1,26 @@ +package io.fairspace.saturn.config.properties; + +import java.util.List; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "application.web-dav") +public class WebDavProperties { + + // Path of the WebDAV's local blob store + private String blobStorePath; + + private ExtraStorage extraStorage; + + @Data + public static class ExtraStorage { + + private String blobStorePath; + + private List defaultRootCollections; + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java new file mode 100644 index 0000000000..0d4213aedd --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/FeaturesController.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.controller; + +import java.util.Set; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.config.enums.Feature; +import io.fairspace.saturn.config.properties.FeatureProperties; + +@RestController +@RequestMapping("/features") +@RequiredArgsConstructor +public class FeaturesController { + + private final FeatureProperties featureProperties; + + @GetMapping("/") + public ResponseEntity> getFeatures() { + return ResponseEntity.ok(featureProperties.getFeatures()); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java new file mode 100644 index 0000000000..0a0e554856 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MaintenanceController.java @@ -0,0 +1,36 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.services.maintenance.MaintenanceService; + +@RestController +@RequestMapping("/maintenance") +@RequiredArgsConstructor +public class MaintenanceController { + + private final MaintenanceService maintenanceService; + + @PostMapping("/reindex") + public ResponseEntity startReindex() { + maintenanceService.startRecreateIndexTask(); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/compact") + public ResponseEntity compactRdfStorage() { + maintenanceService.compactRdfStorageTask(); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/status") + public ResponseEntity getStatus() { + var status = maintenanceService.active() ? "active" : "inactive"; + return ResponseEntity.ok(status); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java new file mode 100644 index 0000000000..2def193c5a --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/MetadataController.java @@ -0,0 +1,101 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ResourceFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.controller.validation.ValidIri; +import io.fairspace.saturn.services.metadata.MetadataService; + +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_LD_JSON; +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_N_TRIPLES; +import static io.fairspace.saturn.controller.enums.CustomMediaType.TEXT_TURTLE; +import static io.fairspace.saturn.services.metadata.Serialization.deserialize; +import static io.fairspace.saturn.services.metadata.Serialization.getFormat; +import static io.fairspace.saturn.services.metadata.Serialization.serialize; + +@Log4j2 +@RestController +@RequestMapping("/metadata") +@RequiredArgsConstructor +@Validated +public class MetadataController { + + private static final String DO_VIEWS_UPDATE = "doViewsUpdate"; + + public static final String DO_VIEWS_UPDATE_DEFAULT_VALUE = "true"; + + private final MetadataService metadataService; + + @GetMapping( + value = "/", + produces = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + public ResponseEntity getMetadata( + @RequestParam(required = false) String subject, + @RequestParam(name = "withValueProperties", defaultValue = "false") boolean withValueProperties, + @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { + var model = metadataService.get(subject, withValueProperties); + var format = getFormat(acceptHeader); + var metadata = serialize(model, format); + return ResponseEntity.ok(metadata); + } + + @PutMapping( + value = "/", + consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void putMetadata( + @RequestBody String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) + boolean doMaterializedViewsRefresh) { + Model model = deserialize(body, contentType); + metadataService.put(model, doMaterializedViewsRefresh); + } + + @PatchMapping( + value = "/", + consumes = {MediaType.APPLICATION_JSON_VALUE, APPLICATION_LD_JSON, TEXT_TURTLE, APPLICATION_N_TRIPLES}) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void patchMetadata( + @RequestBody String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) boolean doViewsUpdate) { + Model model = deserialize(body, contentType); + metadataService.patch(model, doViewsUpdate); + } + + @DeleteMapping("/") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteMetadata( + @RequestParam(required = false) @ValidIri String subject, + @RequestBody(required = false) String body, + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(name = DO_VIEWS_UPDATE, defaultValue = DO_VIEWS_UPDATE_DEFAULT_VALUE) + boolean doMaterializedViewsRefresh) { + if (subject != null) { + if (!metadataService.softDelete(ResourceFactory.createResource(subject))) { + throw new IllegalArgumentException("Subject could not be deleted"); + } + } else { + Model model = deserialize(body, contentType); + metadataService.delete(model, doMaterializedViewsRefresh); + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java new file mode 100644 index 0000000000..7a00e4121c --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SearchController.java @@ -0,0 +1,40 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; +import io.fairspace.saturn.services.search.FileSearchService; +import io.fairspace.saturn.services.search.SearchService; + +@RestController +@RequestMapping("/search") +@RequiredArgsConstructor +public class SearchController { + + private final SearchService searchService; + + private final FileSearchService fileSearchService; + + @PostMapping(value = "/files") + public ResponseEntity searchFiles(@RequestBody FileSearchRequest request) { + var searchResult = fileSearchService.searchFiles(request); + var resultDto = SearchResultsDto.builder() + .results(searchResult) + .query(request.getQuery()) + .build(); + return ResponseEntity.ok(resultDto); + } + + @PostMapping(value = "/lookup") + public ResponseEntity lookupSearch(@RequestBody LookupSearchRequest request) { + var results = searchService.getLookupSearchResults(request); + return ResponseEntity.ok(results); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java new file mode 100644 index 0000000000..10931cdc8a --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/SparqlController.java @@ -0,0 +1,45 @@ +package io.fairspace.saturn.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.controller.validation.ValidSparqlReadQuery; +import io.fairspace.saturn.services.AccessDeniedException; +import io.fairspace.saturn.services.metadata.MetadataPermissions; +import io.fairspace.saturn.services.views.SparqlQueryService; + +import static io.fairspace.saturn.controller.enums.CustomMediaType.APPLICATION_SPARQL_QUERY; + +@RestController +@RequestMapping("/rdf") +@Validated +@RequiredArgsConstructor +public class SparqlController { + + private final SparqlQueryService sparqlQueryService; + + private final MetadataPermissions metadataPermissions; + + /** + * Execute a read-only SPARQL query. + * + * @param sparqlQuery the SPARQL query + * @return the result of the query (JSON) + */ + @PostMapping(value = "/query", consumes = APPLICATION_SPARQL_QUERY) + // todo: uncomment the line below and remove the metadataPermissions.hasMetadataQueryPermission() call once + // the MetadataPermissions is available in the IoC container + // @PreAuthorize("@metadataPermissions.hasMetadataQueryPermission()") + public ResponseEntity executeSparqlQuery(@ValidSparqlReadQuery @RequestBody String sparqlQuery) { + if (!metadataPermissions.hasMetadataQueryPermission()) { + throw new AccessDeniedException("You do not have permission to execute SPARQL queries."); + } + var json = sparqlQueryService.executeQuery(sparqlQuery); + return ResponseEntity.ok(json); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java new file mode 100644 index 0000000000..3789614e69 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/UserController.java @@ -0,0 +1,36 @@ +package io.fairspace.saturn.controller; + +import java.util.Collection; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import io.fairspace.saturn.services.users.User; +import io.fairspace.saturn.services.users.UserRolesUpdate; +import io.fairspace.saturn.services.users.UserService; + +@RestController +@RequestMapping("/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @GetMapping("/") + public ResponseEntity> getUsers() { + return ResponseEntity.ok(userService.getUsers()); + } + + @PatchMapping("/") + public ResponseEntity updateUserRoles(@RequestBody UserRolesUpdate update) { + userService.update(update); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/current") + public ResponseEntity getCurrentUser() { + var currentUser = userService.currentUser(); + return ResponseEntity.ok(currentUser); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java new file mode 100644 index 0000000000..6c9f020264 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/ViewController.java @@ -0,0 +1,59 @@ +package io.fairspace.saturn.controller; + +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.FacetsDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.ViewsDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; +import io.fairspace.saturn.services.views.QueryService; +import io.fairspace.saturn.services.views.ViewService; + +@RestController +@RequestMapping("/views") +@Validated +public class ViewController { + + private final ViewService viewService; + + private final QueryService services; + + public ViewController(ViewService viewService, @Qualifier("queryService") QueryService services) { + this.viewService = viewService; + this.services = services; + } + + @GetMapping("/") + public ResponseEntity getViews() { + var views = viewService.getViews(); + return ResponseEntity.ok(new ViewsDto(views)); + } + + @PostMapping("/") + public ResponseEntity getViewData(@Valid @RequestBody ViewRequest requestBody) { + var result = services.retrieveViewPage(requestBody); + return ResponseEntity.ok(result); + } + + @GetMapping("/facets") + public ResponseEntity getFacets() { + var facets = viewService.getFacets(); + return ResponseEntity.ok(new FacetsDto(facets)); + } + + @PostMapping("/count") + public ResponseEntity count(@Valid @RequestBody CountRequest requestBody) { + var result = services.count(requestBody); + return ResponseEntity.ok(result); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java new file mode 100644 index 0000000000..f7a2f998d9 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/VocabularyController.java @@ -0,0 +1,37 @@ +package io.fairspace.saturn.controller; + +import org.apache.jena.rdf.model.Model; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static io.fairspace.saturn.services.metadata.Serialization.getFormat; +import static io.fairspace.saturn.services.metadata.Serialization.serialize; + +@RestController +@RequestMapping("/vocabulary") +public class VocabularyController { + + private final Model vocabulary; + + public VocabularyController(@Qualifier("vocabulary") Model vocabulary) { + this.vocabulary = vocabulary; + } + + @GetMapping("/") + public ResponseEntity getVocabulary( + @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader) { + var format = getFormat(acceptHeader); + var contentType = format.getLang().getHeaderString(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType(contentType)); + var serializedVocabulary = serialize(vocabulary, format); + return new ResponseEntity<>(serializedVocabulary, headers, HttpStatus.OK); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java new file mode 100644 index 0000000000..dadc2d2762 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/WorkspaceController.java @@ -0,0 +1,66 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; + +import lombok.RequiredArgsConstructor; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.fairspace.saturn.services.workspaces.UserRoleDto; +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceRole; +import io.fairspace.saturn.services.workspaces.WorkspaceService; + +@RestController +@RequestMapping("/workspaces") +@Validated +@RequiredArgsConstructor +public class WorkspaceController { + + private final WorkspaceService workspaceService; + + @PutMapping("/") + public ResponseEntity createWorkspace(@RequestBody Workspace workspace) { + var createdWorkspace = workspaceService.createWorkspace(workspace); + return ResponseEntity.ok( + createdWorkspace); // it should return HTTP 201 CREATED - tobe analyzed across the codebase + } + + @GetMapping("/") + public ResponseEntity> listWorkspaces() { + var workspaces = workspaceService.listWorkspaces(); + return ResponseEntity.ok(workspaces); + } + + @DeleteMapping("/") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteWorkspace(@RequestParam("workspace") String workspaceUri) { + workspaceService.deleteWorkspace(NodeFactory.createURI(workspaceUri)); + } + + @GetMapping(value = "/users/") + public ResponseEntity> getUsers(@RequestParam("workspace") String workspaceUri) { + var uri = NodeFactory.createURI(workspaceUri); + var users = workspaceService.getUsers(uri); + return ResponseEntity.ok(users); + } + + @PatchMapping(value = "/users/") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void setUserRole(@RequestBody UserRoleDto userRoleDto) { + workspaceService.setUserRole(userRoleDto.getWorkspace(), userRoleDto.getUser(), userRoleDto.getRole()); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java new file mode 100644 index 0000000000..e83a542acc --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ColumnDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import io.fairspace.saturn.config.properties.ViewsProperties; + +public record ColumnDto(String name, String title, ViewsProperties.ColumnType type, Integer displayIndex) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java new file mode 100644 index 0000000000..6056f40290 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/CountDto.java @@ -0,0 +1,3 @@ +package io.fairspace.saturn.controller.dto; + +public record CountDto(long count, boolean timeout) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java new file mode 100644 index 0000000000..0741c49902 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ErrorDto.java @@ -0,0 +1,12 @@ +package io.fairspace.saturn.controller.dto; + +import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldProperty; +import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldType; + +import io.fairspace.saturn.vocabulary.FS; + +@JsonldType(FS.ERROR_URI) +public record ErrorDto( + @JsonldProperty(FS.ERROR_STATUS_URI) int status, + @JsonldProperty(FS.ERROR_MESSAGE_URI) String message, + @JsonldProperty(FS.ERROR_DETAILS_URI) Object details) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java new file mode 100644 index 0000000000..708a127585 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetDto.java @@ -0,0 +1,19 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import io.fairspace.saturn.config.properties.ViewsProperties; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; + +@JsonInclude(NON_NULL) +public record FacetDto( + String name, + String title, + ViewsProperties.ColumnType type, + List values, + Boolean booleanValue, + Object min, + Object max) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java new file mode 100644 index 0000000000..93c1797406 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/FacetsDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +public record FacetsDto(List facets) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java new file mode 100644 index 0000000000..9d86207112 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultDto.java @@ -0,0 +1,7 @@ +package io.fairspace.saturn.controller.dto; + +import lombok.Builder; +import lombok.NonNull; + +@Builder +public record SearchResultDto(@NonNull String id, String label, String type, String comment) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java new file mode 100644 index 0000000000..4875192363 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/SearchResultsDto.java @@ -0,0 +1,8 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import lombok.Builder; + +@Builder +public record SearchResultsDto(List results, String query) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java new file mode 100644 index 0000000000..209f450245 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ValueDto.java @@ -0,0 +1,8 @@ +package io.fairspace.saturn.controller.dto; + +public record ValueDto(String label, Object value) implements Comparable { + @Override + public int compareTo(ValueDto o) { + return label.compareTo(o.label); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java new file mode 100644 index 0000000000..23a23843e9 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewDto.java @@ -0,0 +1,11 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +public record ViewDto( + String name, + String title, + List columns, + @JsonInclude(JsonInclude.Include.NON_NULL) Long maxDisplayCount) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java similarity index 70% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java index 1d9eb16d91..3e89f0a43d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewPageDTO.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewPageDto.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto; import java.util.List; import java.util.Map; @@ -8,12 +8,12 @@ @Value @Builder -public class ViewPageDTO { +public class ViewPageDto { /** * The key of every row is `${view}_${column}`. */ @NonNull - List>> rows; + List>> rows; boolean hasNext; boolean timeout; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java new file mode 100644 index 0000000000..7ceb437c4e --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/ViewsDto.java @@ -0,0 +1,5 @@ +package io.fairspace.saturn.controller.dto; + +import java.util.List; + +public record ViewsDto(List views) {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java similarity index 57% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java index 6915209678..d2a443ec38 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/CountRequest.java @@ -1,13 +1,13 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto.request; import java.util.List; import jakarta.validation.constraints.NotBlank; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; -@Getter -@Setter +import io.fairspace.saturn.services.views.ViewFilter; + +@Data public class CountRequest { @NotBlank private String view; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java similarity index 58% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java index 095a659e63..fbac7e815c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/FileSearchRequest.java @@ -1,12 +1,10 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; -import jakarta.validation.constraints.NotBlank; import lombok.Getter; import lombok.Setter; @Getter @Setter public class FileSearchRequest extends SearchRequest { - @NotBlank private String parentIRI; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java similarity index 74% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java index 2f56fad35b..269a4171ed 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/LookupSearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/LookupSearchRequest.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; import lombok.Getter; import lombok.Setter; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java similarity index 69% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java index 9a1c5493dd..42f67e9c1c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/SearchRequest.java @@ -1,4 +1,4 @@ -package io.fairspace.saturn.services.search; +package io.fairspace.saturn.controller.dto.request; import lombok.Getter; import lombok.Setter; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java similarity index 75% rename from projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java rename to projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java index d3801c2ee7..6ea4eb716f 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/dto/request/ViewRequest.java @@ -1,11 +1,11 @@ -package io.fairspace.saturn.services.views; +package io.fairspace.saturn.controller.dto.request; import jakarta.validation.constraints.Min; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.EqualsAndHashCode; -@Getter -@Setter +@EqualsAndHashCode(callSuper = true) +@Data public class ViewRequest extends CountRequest { @Min(1) private Integer page; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java new file mode 100644 index 0000000000..cde1da8f21 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/enums/CustomMediaType.java @@ -0,0 +1,13 @@ +package io.fairspace.saturn.controller.enums; + +public enum CustomMediaType { + ; + + public static final String APPLICATION_LD_JSON = "application/ld+json"; + + public static final String TEXT_TURTLE = "text/turtle"; + + public static final String APPLICATION_N_TRIPLES = "application/n-triples"; + + public static final String APPLICATION_SPARQL_QUERY = "application/sparql-query"; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000..d8ac294b15 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandler.java @@ -0,0 +1,58 @@ +package io.fairspace.saturn.controller.exception; + +import java.util.stream.Collectors; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import io.fairspace.saturn.controller.dto.ErrorDto; +import io.fairspace.saturn.services.metadata.validation.ValidationException; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException( + ConstraintViolationException ex, HttpServletRequest req) { + var violations = ex.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .sorted() + .collect(Collectors.joining("; ")); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", "Violations: " + violations); + } + + @ExceptionHandler(ValidationException.class) + public ResponseEntity handleValidationException(ValidationException ex, HttpServletRequest req) { + log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getViolations()); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException( + IllegalArgumentException ex, HttpServletRequest req) { + log.error("Validation error for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.BAD_REQUEST, "Validation Error", ex.getMessage()); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex, HttpServletRequest req) { + log.error("Access denied for request {} {}", req.getMethod(), req.getRequestURI(), ex); + return buildErrorResponse(HttpStatus.FORBIDDEN, "Access Denied"); + } + + private ResponseEntity buildErrorResponse(HttpStatus status, String message) { + return ResponseEntity.status(status).body(new ErrorDto(status.value(), message, null)); + } + + private ResponseEntity buildErrorResponse(HttpStatus status, String message, Object info) { + return ResponseEntity.status(status).body(new ErrorDto(status.value(), message, info)); + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java new file mode 100644 index 0000000000..e6bc0371ce --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/IriValidator.java @@ -0,0 +1,30 @@ +package io.fairspace.saturn.controller.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.extern.slf4j.Slf4j; + +import static org.apache.jena.riot.system.Checker.checkIRI; + +/** + * Validates that a given IRI is valid. + */ +@Slf4j +public class IriValidator implements ConstraintValidator { + @Override + public boolean isValid(String subject, ConstraintValidatorContext context) { + try { + var isValid = subject == null || checkIRI(subject); + if (!isValid) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate( + String.format(context.getDefaultConstraintMessageTemplate(), subject)) + .addConstraintViolation(); + } + return isValid; + } catch (Exception e) { + log.error("Error validating IRI", e); + return false; + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/SparqlReadQueryValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/SparqlReadQueryValidator.java new file mode 100644 index 0000000000..32d054cefd --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/SparqlReadQueryValidator.java @@ -0,0 +1,25 @@ +package io.fairspace.saturn.controller.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.Syntax; + +/** + * Validates that a given SPARQL query is a read-only query. + */ +@Slf4j +public class SparqlReadQueryValidator implements ConstraintValidator { + @Override + public boolean isValid(String sparqlQuery, ConstraintValidatorContext constraintValidatorContext) { + try { + Query query = QueryFactory.create(sparqlQuery, Syntax.syntaxARQ); + return query.isSelectType() || query.isAskType() || query.isConstructType() || query.isDescribeType(); + } catch (Exception e) { + log.error("Error validating SPARQL query", e); + return false; + } + } +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java new file mode 100644 index 0000000000..1e3609f211 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidIri.java @@ -0,0 +1,21 @@ +package io.fairspace.saturn.controller.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = IriValidator.class) +public @interface ValidIri { + + String message() default "Invalid IRI: %s"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidSparqlReadQuery.java b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidSparqlReadQuery.java new file mode 100644 index 0000000000..3abd0fd436 --- /dev/null +++ b/projects/saturn/src/main/java/io/fairspace/saturn/controller/validation/ValidSparqlReadQuery.java @@ -0,0 +1,21 @@ +package io.fairspace.saturn.controller.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = SparqlReadQueryValidator.class) +public @interface ValidSparqlReadQuery { + + String message() default "Invalid SPARQL query. Only SELECT, ASK, CONSTRUCT, and DESCRIBE queries are allowed."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractChangesAwareDatasetGraph.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractChangesAwareDatasetGraph.java index d25a02cf05..faf541ec4b 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractChangesAwareDatasetGraph.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractChangesAwareDatasetGraph.java @@ -1,9 +1,9 @@ package io.fairspace.saturn.rdf; import org.apache.jena.graph.Node; +import org.apache.jena.query.text.changes.DatasetGraphTextMonitor; +import org.apache.jena.query.text.changes.TextQuadAction; import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.sparql.core.DatasetGraphMonitor; -import org.apache.jena.sparql.core.QuadAction; import static java.lang.Integer.toHexString; import static java.lang.System.identityHashCode; @@ -15,7 +15,7 @@ * That can be very inconvenient, if you need to implement complex logic involving not only quad operations, but also * some other aspect's of the dataset graph;s behavior, e.g. transaction lifecycle. */ -public abstract class AbstractChangesAwareDatasetGraph extends DatasetGraphMonitor { +public abstract class AbstractChangesAwareDatasetGraph extends DatasetGraphTextMonitor { public AbstractChangesAwareDatasetGraph(DatasetGraph dsg) { super(dsg, new DelegatingDatasetChanges(), true); @@ -23,7 +23,7 @@ public AbstractChangesAwareDatasetGraph(DatasetGraph dsg) { ((DelegatingDatasetChanges) getMonitor()).setChangeListener(this::onChange); // delegates handling to itself } - protected void onChange(QuadAction action, Node graph, Node subject, Node predicate, Node object) {} + protected void onChange(TextQuadAction action, Node graph, Node subject, Node predicate, Node object) {} @Override public String toString() { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractDatasetChanges.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractDatasetChanges.java index 99b2c2c777..7e0dbffeb7 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractDatasetChanges.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/AbstractDatasetChanges.java @@ -1,11 +1,11 @@ package io.fairspace.saturn.rdf; -import org.apache.jena.sparql.core.DatasetChanges; +import org.apache.jena.query.text.changes.TextDatasetChanges; /** * A class providing default implementations for rarely used methods of DatasetChanges */ -public abstract class AbstractDatasetChanges implements DatasetChanges { +public abstract class AbstractDatasetChanges implements TextDatasetChanges { @Override public void start() {} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/DelegatingDatasetChanges.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/DelegatingDatasetChanges.java index aed2f4321e..02bbb3a26a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/DelegatingDatasetChanges.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/DelegatingDatasetChanges.java @@ -1,7 +1,7 @@ package io.fairspace.saturn.rdf; import org.apache.jena.graph.Node; -import org.apache.jena.sparql.core.QuadAction; +import org.apache.jena.query.text.changes.TextQuadAction; public class DelegatingDatasetChanges extends AbstractDatasetChanges { private GraphChangeListener changeListener; @@ -17,7 +17,7 @@ public void setChangeListener(GraphChangeListener changeListener) { } @Override - public void change(QuadAction action, Node graph, Node subject, Node predicate, Node object) { + public void change(TextQuadAction action, Node graph, Node subject, Node predicate, Node object) { if (changeListener != null) { changeListener.onChange(action, graph, subject, predicate, object); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/GraphChangeListener.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/GraphChangeListener.java index 55eaf89a88..9f5f681f57 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/GraphChangeListener.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/GraphChangeListener.java @@ -1,8 +1,8 @@ package io.fairspace.saturn.rdf; import org.apache.jena.graph.Node; -import org.apache.jena.sparql.core.QuadAction; +import org.apache.jena.query.text.changes.TextQuadAction; interface GraphChangeListener { - void onChange(QuadAction action, Node graph, Node subject, Node predicate, Node object); + void onChange(TextQuadAction action, Node graph, Node subject, Node predicate, Node object); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java index 7b50f5f136..ab885172ff 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SaturnDatasetFactory.java @@ -2,15 +2,19 @@ import java.io.File; -import lombok.extern.log4j.*; +import lombok.extern.log4j.Log4j2; import org.apache.jena.datatypes.TypeMapper; import org.apache.jena.dboe.base.file.Location; import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; -import io.fairspace.saturn.config.*; -import io.fairspace.saturn.rdf.transactions.*; -import io.fairspace.saturn.services.views.*; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.rdf.transactions.LocalTransactionLog; +import io.fairspace.saturn.rdf.transactions.SparqlTransactionCodec; +import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; +import io.fairspace.saturn.rdf.transactions.TxnLogDatasetGraph; +import io.fairspace.saturn.services.views.ViewStoreClientFactory; import static io.fairspace.saturn.rdf.MarkdownDataType.MARKDOWN_DATA_TYPE; import static io.fairspace.saturn.rdf.transactions.Restore.restore; @@ -28,17 +32,24 @@ public class SaturnDatasetFactory { * Currently it adds transaction logging and applies default vocabulary if * needed. */ - public static Dataset connect(Config.Jena config, ViewStoreClientFactory viewStoreClientFactory) { - var restoreNeeded = isRestoreNeeded(config.datasetPath); + public static Dataset connect( + ViewsProperties viewsProperties, + JenaProperties jenaProperties, + ViewStoreClientFactory viewStoreClientFactory, + String publicUrl) { + var restoreNeeded = isRestoreNeeded(jenaProperties.getDatasetPath()); // Create a TDB2 dataset graph - var dsg = connectCreate(Location.create(config.datasetPath.getAbsolutePath()), config.storeParams, null) + var dsg = connectCreate( + Location.create(jenaProperties.getDatasetPath().getAbsolutePath()), + jenaProperties.getStoreParams(), + null) .getDatasetGraph(); - var txnLog = new LocalTransactionLog(config.transactionLogPath, new SparqlTransactionCodec()); + var txnLog = new LocalTransactionLog(jenaProperties.getTransactionLogPath(), new SparqlTransactionCodec()); if (viewStoreClientFactory != null) { - dsg = new TxnIndexDatasetGraph(dsg, viewStoreClientFactory); + dsg = new TxnIndexDatasetGraph(viewsProperties, dsg, viewStoreClientFactory, publicUrl); } if (restoreNeeded) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java index 390911fe6f..f3975d9579 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/SparqlUtils.java @@ -3,37 +3,51 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.*; +import java.util.ArrayList; +import java.util.GregorianCalendar; +import java.util.List; import java.util.function.Consumer; import lombok.extern.log4j.Log4j2; import org.apache.jena.datatypes.xsd.XSDDateTime; import org.apache.jena.graph.Node; -import org.apache.jena.query.*; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.QuerySolutionMap; import org.apache.jena.rdf.model.Literal; import org.apache.jena.rdf.model.Resource; -import org.apache.jena.sparql.core.*; +import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.update.UpdateExecutionFactory; import org.apache.jena.update.UpdateFactory; -import io.fairspace.saturn.services.search.SearchResultDTO; - -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.controller.dto.SearchResultDto; import static java.util.Optional.ofNullable; import static java.util.UUID.randomUUID; import static org.apache.jena.graph.NodeFactory.createURI; import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; -import static org.apache.jena.system.Txn.*; +import static org.apache.jena.system.Txn.calculateRead; +import static org.apache.jena.system.Txn.executeRead; +import static org.apache.jena.system.Txn.executeWrite; @Log4j2 public class SparqlUtils { + public static Node generateMetadataIri() { - return generateMetadataIri(randomUUID().toString()); + return generateMetadataIriFromId(randomUUID().toString()); + } + + public static Node generateMetadataIriFromId(String id) { + return createURI(JenaProperties.getMetadataBaseIri() + id); } - public static Node generateMetadataIri(String id) { - return createURI(CONFIG.jena.metadataBaseIRI + id); + public static Node generateMetadataIriFromUri(String uri) { + return createURI(uri); } public static Instant parseXSDDateTimeLiteral(Literal literal) { @@ -45,11 +59,9 @@ public static Literal toXSDDateTimeLiteral(Instant instant) { return createTypedLiteral(GregorianCalendar.from(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()))); } + // TODO: it should be a part of data layer, not utils /** * Execute a SELECT query and process the rows of the results with the handler code. - * - * @param query - * @param rowAction */ public static void querySelect(DatasetGraph dsg, String query, Consumer rowAction) { executeRead(dsg, () -> { @@ -72,39 +84,39 @@ public static String getQueryRegex(String query) { .replace("/\\/g", "\\\\"); } - public static List getByQuery(Query query, QuerySolutionMap binding, Dataset dataset) { + public static List getByQuery(Query query, QuerySolutionMap binding, Dataset dataset) { log.debug("Executing query:\n{}", query); - var selectExecution = QueryExecutionFactory.create(query, dataset, binding); - var results = new ArrayList(); - - return calculateRead(dataset, () -> { - try (selectExecution) { - //noinspection NullableProblems - for (var row : (Iterable) selectExecution::execSelect) { - var id = row.getResource("id").getURI(); - var label = row.getLiteral("label").getString(); - var type = ofNullable(row.getResource("type")) - .map(Resource::getURI) - .orElse(null); - var comment = ofNullable(row.getLiteral("comment")) - .map(Literal::getString) - .orElse(null); - - var dto = SearchResultDTO.builder() - .id(id) - .label(label) - .type(type) - .comment(comment) - .build(); - results.add(dto); + try (var selectExecution = QueryExecutionFactory.create(query, dataset, binding)) { + var results = new ArrayList(); + + return calculateRead(dataset, () -> { + try (selectExecution) { + for (var row : (Iterable) selectExecution::execSelect) { + var id = row.getResource("id").getURI(); + var label = row.getLiteral("label").getString(); + var type = ofNullable(row.getResource("type")) + .map(Resource::getURI) + .orElse(null); + var comment = ofNullable(row.getLiteral("comment")) + .map(Literal::getString) + .orElse(null); + + var dto = SearchResultDto.builder() + .id(id) + .label(label) + .type(type) + .comment(comment) + .build(); + results.add(dto); + } + } catch (Exception e) { + String message = "Error executing select query: \n %s".formatted(query.toString()); + log.error(message, e); + throw new RuntimeException(message, e); } - } catch (Exception e) { - String message = "Error executing select query: \n %s".formatted(query.toString()); - log.error(message, e); - throw new RuntimeException(message, e); - } - return results; - }); + return results; + }); + } } private static QueryExecution query(DatasetGraph dsg, String query) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/dao/DAO.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/dao/DAO.java index 76706037c7..6b6500449a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/dao/DAO.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/dao/DAO.java @@ -109,7 +109,7 @@ public T write(T entity) { entity.setIri(generateMetadataIri()); } - graph.add(new Triple(entity.getIri(), RDF.type.asNode(), type)); + graph.add(Triple.create(entity.getIri(), RDF.type.asNode(), type)); processFields(entity.getClass(), (field, annotation) -> { var propertyNode = createURI(annotation.value()); @@ -123,9 +123,10 @@ public T write(T entity) { if (value instanceof Iterable) { ((Iterable) value) - .forEach(item -> graph.add(new Triple(entity.getIri(), propertyNode, valueToNode(item)))); + .forEach( + item -> graph.add(Triple.create(entity.getIri(), propertyNode, valueToNode(item)))); } else if (value != null) { - graph.add(new Triple(entity.getIri(), propertyNode, valueToNode(value))); + graph.add(Triple.create(entity.getIri(), propertyNode, valueToNode(value))); } }); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/BulkTransactions.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/BulkTransactions.java index b6b83086c1..044283a56d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/BulkTransactions.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/BulkTransactions.java @@ -7,12 +7,15 @@ import java.util.concurrent.atomic.AtomicInteger; import com.pivovarit.function.ThrowingFunction; +import jakarta.servlet.http.HttpServletRequest; import org.apache.jena.query.Dataset; import org.apache.jena.query.ReadWrite; import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.JenaTransactionException; import org.apache.jena.system.Txn; -import org.eclipse.jetty.server.Request; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static io.fairspace.saturn.auth.RequestContext.getCurrentUserStringUri; @@ -21,6 +24,12 @@ import static java.lang.Thread.currentThread; +@Component +@ConditionalOnProperty( + name = "application.jena.bulkTransactions", + havingValue = "true", + matchIfMissing = true // BulkTransactions is used by default + ) public class BulkTransactions extends BaseTransactions { private final LinkedBlockingQueue> queue = new LinkedBlockingQueue<>(); private static final AtomicInteger threadCounter = new AtomicInteger(); @@ -43,7 +52,7 @@ public class BulkTransactions extends BaseTransactions { }, "Batch transaction processor " + threadCounter.incrementAndGet()); - public BulkTransactions(Dataset ds) { + public BulkTransactions(@Qualifier("dataset") Dataset ds) { super(ds); worker.start(); @@ -105,13 +114,13 @@ public void close() throws Exception { private static class Task { private final CountDownLatch canBeRead = new CountDownLatch(1); - private final Request request; + private final HttpServletRequest request; private final String userUri; private final ThrowingFunction job; private R result; private Throwable error; - Task(Request request, String userUri, ThrowingFunction job) { + Task(HttpServletRequest request, String userUri, ThrowingFunction job) { this.request = request; this.userUri = userUri; this.job = job; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/SimpleTransactions.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/SimpleTransactions.java index 2ce792d21f..829859a5a3 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/SimpleTransactions.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/SimpleTransactions.java @@ -4,10 +4,15 @@ import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.system.Txn; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +@Component +@ConditionalOnProperty(name = "application.jena.bulkTransactions", havingValue = "false") public class SimpleTransactions extends BaseTransactions { - public SimpleTransactions(Dataset ds) { + public SimpleTransactions(@Qualifier("dataset") Dataset ds) { super(ds); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java index 9667f06b81..330439ae67 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnIndexDatasetGraph.java @@ -9,29 +9,38 @@ import org.apache.jena.graph.Node; import org.apache.jena.query.ReadWrite; import org.apache.jena.query.TxnType; +import org.apache.jena.query.text.changes.TextQuadAction; import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.sparql.core.QuadAction; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.AbstractChangesAwareDatasetGraph; import io.fairspace.saturn.services.views.ViewStoreClientFactory; import io.fairspace.saturn.services.views.ViewUpdater; -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; import static io.fairspace.saturn.services.users.UserService.currentUserAsSymbol; @Slf4j public class TxnIndexDatasetGraph extends AbstractChangesAwareDatasetGraph { + + private final ViewsProperties viewsProperties; private final DatasetGraph dsg; private final ViewStoreClientFactory viewStoreClientFactory; + private final String publicUrl; // One set of updated subjects if write transactions are handled sequentially. // If many write transactions can be active simultaneously, this set needs to be // tied to the active thread. private final Set updatedSubjects = new HashSet<>(); - public TxnIndexDatasetGraph(DatasetGraph dsg, ViewStoreClientFactory viewStoreClientFactory) { + public TxnIndexDatasetGraph( + ViewsProperties viewsProperties, + DatasetGraph dsg, + ViewStoreClientFactory viewStoreClientFactory, + String publicUrl) { super(dsg); + this.viewsProperties = viewsProperties; this.dsg = dsg; this.viewStoreClientFactory = viewStoreClientFactory; + this.publicUrl = publicUrl; } private void markSubject(Node subject) { @@ -42,7 +51,7 @@ private void markSubject(Node subject) { * Collects changes */ @Override - protected void onChange(QuadAction action, Node graph, Node subject, Node predicate, Node object) { + protected void onChange(TextQuadAction action, Node graph, Node subject, Node predicate, Node object) { switch (action) { case ADD, DELETE -> markSubject(subject); } @@ -74,7 +83,7 @@ public void commit() { log.info("Commit {} updated subjects", updatedSubjects.size()); var start = new Date().getTime(); try (var viewStoreClient = viewStoreClientFactory.build(); - var viewUpdater = new ViewUpdater(viewStoreClient, dsg)) { + var viewUpdater = new ViewUpdater(viewsProperties, viewStoreClient, dsg, publicUrl)) { updatedSubjects.forEach(viewUpdater::updateSubject); viewUpdater.commit(); log.debug( @@ -112,6 +121,6 @@ private boolean isInWriteTransaction() { private boolean isExtraStorageTransaction() { return updatedSubjects.stream() - .anyMatch(r -> r.isURI() && r.getURI().startsWith(CONFIG.publicUrl + "/api/extra-storage")); + .anyMatch(r -> r.isURI() && r.getURI().startsWith(publicUrl + "/api/extra-storage")); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java index 0f7442fb9c..e03702bc43 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraph.java @@ -1,17 +1,17 @@ package io.fairspace.saturn.rdf.transactions; import com.pivovarit.function.ThrowingRunnable; -import lombok.extern.log4j.*; +import lombok.extern.log4j.Log4j2; import org.apache.jena.graph.Node; import org.apache.jena.query.ReadWrite; import org.apache.jena.query.TxnType; +import org.apache.jena.query.text.changes.TextQuadAction; import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.sparql.core.QuadAction; -import org.keycloak.representations.AccessToken; +import io.fairspace.saturn.auth.RequestContext; import io.fairspace.saturn.rdf.AbstractChangesAwareDatasetGraph; -import static io.fairspace.saturn.auth.RequestContext.getAccessToken; +import static io.fairspace.saturn.auth.RequestContext.getClaims; import static java.lang.System.currentTimeMillis; @@ -21,7 +21,7 @@ public class TxnLogDatasetGraph extends AbstractChangesAwareDatasetGraph { "Catastrophic failure. Shutting down. The system requires admin's intervention."; private final TransactionLog transactionLog; - private volatile AccessToken user; + private volatile RequestContext.SaturnClaims claims; private DatasetGraph dsg; public TxnLogDatasetGraph(DatasetGraph dsg, TransactionLog transactionLog) { @@ -34,12 +34,12 @@ public TxnLogDatasetGraph(DatasetGraph dsg, TransactionLog transactionLog) { * Collects changes */ @Override - protected void onChange(QuadAction action, Node graph, Node subject, Node predicate, Node object) { + protected void onChange(TextQuadAction action, Node graph, Node subject, Node predicate, Node object) { critical(() -> { - var currentUser = getAccessToken(); - if (currentUser != user) { - user = currentUser; - transactionLog.onMetadata(user.getSubject(), user.getName(), currentTimeMillis()); + var newClaims = getClaims(); + if (!newClaims.equals(claims)) { + this.claims = newClaims; + transactionLog.onMetadata(this.claims.getSubject(), this.claims.getName(), currentTimeMillis()); } switch (action) { case ADD -> transactionLog.onAdd(graph, subject, predicate, object); @@ -65,7 +65,7 @@ public void begin(ReadWrite readWrite) { super.begin(readWrite); if (readWrite == ReadWrite.WRITE) { // a write transaction => be ready to collect changes - user = null; + claims = null; critical(transactionLog::onBegin); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java deleted file mode 100644 index e2f9568ba2..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/BaseApp.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.fairspace.saturn.services; - -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.extern.log4j.*; -import spark.*; -import spark.servlet.SparkApplication; - -import io.fairspace.saturn.rdf.dao.DAOException; -import io.fairspace.saturn.util.UnsupportedMediaTypeException; - -import static io.fairspace.saturn.services.errors.ErrorHelper.errorBody; -import static io.fairspace.saturn.services.errors.ErrorHelper.exceptionHandler; - -import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; -import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; -import static javax.servlet.http.HttpServletResponse.*; -import static spark.Spark.notFound; -import static spark.Spark.path; -import static spark.globalstate.ServletFlag.isRunningFromServlet; - -@Log4j2 -public abstract class BaseApp implements SparkApplication { - protected static final ObjectMapper mapper = new ObjectMapper() - .registerModule(new IRIModule()) - .registerModule(new JavaTimeModule()) - .configure(WRITE_DATES_AS_TIMESTAMPS, false) - .configure(FAIL_ON_UNKNOWN_PROPERTIES, false); - - private final String basePath; - - protected BaseApp(String basePath) { - this.basePath = basePath; - } - - @Override - public final void init() { - path(basePath, () -> { - notFound((req, res) -> { - String pathInfo = req.pathInfo(); - if (pathInfo.startsWith("/api/webdav") - || pathInfo.startsWith("/api/extra-storage") - || pathInfo.startsWith("/api/rdf")) { - return null; - } - return errorBody(SC_NOT_FOUND, "Not found"); - }); - exception(JsonMappingException.class, exceptionHandler(SC_BAD_REQUEST, "Invalid request body")); - exception(IllegalArgumentException.class, exceptionHandler(SC_BAD_REQUEST, null)); - exception(DAOException.class, exceptionHandler(SC_BAD_REQUEST, "Bad request")); - exception(UnsupportedMediaTypeException.class, exceptionHandler(SC_UNSUPPORTED_MEDIA_TYPE, null)); - exception(AccessDeniedException.class, exceptionHandler(SC_FORBIDDEN, null)); - exception(Exception.class, exceptionHandler(SC_INTERNAL_SERVER_ERROR, "Internal server error")); - exception(NotAvailableException.class, exceptionHandler(SC_SERVICE_UNAVAILABLE, null)); - exception(ConflictException.class, exceptionHandler(SC_CONFLICT, null)); - - initApp(); - }); - } - - protected abstract void initApp(); - - // A temporary workaround for https://github.com/perwendel/spark/issues/1062 - // Shadows spark.Spark.exception - public static void exception(Class exceptionClass, ExceptionHandler handler) { - if (isRunningFromServlet()) { - var wrapper = new ExceptionHandlerImpl<>(exceptionClass) { - @Override - public void handle(T exception, Request request, Response response) { - handler.handle(exception, request, response); - } - }; - - ExceptionMapper.getServletInstance().map(exceptionClass, wrapper); - } else { - Spark.exception(exceptionClass, handler); - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java deleted file mode 100644 index 7fc4e4d7b2..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/PayloadParsingException.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.fairspace.saturn.services; - -/** - * Can represent an error that happened during parsing of HTTP request body, etc - */ -public class PayloadParsingException extends RuntimeException { - public PayloadParsingException() {} - - public PayloadParsingException(String message) { - super(message); - } - - public PayloadParsingException(String message, Throwable cause) { - super(message, cause); - } - - public PayloadParsingException(Throwable cause) { - super(cause); - } - - public PayloadParsingException( - String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { - super(message, cause, enableSuppression, writableStackTrace); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java deleted file mode 100644 index e841b7488f..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorDto.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldProperty; -import ioinformarics.oss.jackson.module.jsonld.annotation.JsonldType; -import lombok.Value; - -import io.fairspace.saturn.vocabulary.FS; - -@Value -@JsonldType(FS.ERROR_URI) -public class ErrorDto { - @JsonldProperty(FS.ERROR_STATUS_URI) - private int status; - - @JsonldProperty(FS.ERROR_MESSAGE_URI) - private String message; - - @JsonldProperty(FS.ERROR_DETAILS_URI) - private Object details; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java deleted file mode 100644 index 4189783165..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/errors/ErrorHelper.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import ioinformarics.oss.jackson.module.jsonld.JsonldModule; -import lombok.SneakyThrows; -import lombok.extern.log4j.*; -import spark.ExceptionHandler; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; - -@Log4j2 -public class ErrorHelper { - private static final ObjectMapper mapper = new ObjectMapper().registerModule(new JsonldModule()); - - public static ExceptionHandler exceptionHandler(int status, String message) { - return (e, req, res) -> { - log.error("{} Error handling request {} {}", status, req.requestMethod(), req.uri(), e); - res.status(status); - res.type(APPLICATION_JSON.asString()); - res.body(errorBody(status, message != null ? message : e.getMessage())); - }; - } - - public static String errorBody(int status, String message) { - return errorBody(status, message, null); - } - - @SneakyThrows(JsonProcessingException.class) - public static String errorBody(int status, String message, Object info) { - return mapper.writeValueAsString(new ErrorDto(status, message, info)); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java deleted file mode 100644 index b6aa9b50eb..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/features/FeaturesApp.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.fairspace.saturn.services.features; - -import java.util.Set; - -import io.fairspace.saturn.config.Feature; -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.get; - -public class FeaturesApp extends BaseApp { - private final Set features; - - public FeaturesApp(String basePath, Set features) { - super(basePath); - this.features = features; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(features); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/Health.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/health/Health.java deleted file mode 100644 index 74a14aa29e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/Health.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.fairspace.saturn.services.health; - -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class Health { - @NotNull - private HealthStatus status = HealthStatus.UP; - - @JsonSetter(nulls = Nulls.AS_EMPTY) - private Map components = new HashMap<>(); -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthApp.java deleted file mode 100644 index 364b457352..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthApp.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.fairspace.saturn.services.health; - -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.get; - -public class HealthApp extends BaseApp { - private final HealthService healthService; - - public HealthApp(String basePath, HealthService healthService) { - super(basePath); - this.healthService = healthService; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - Health health = healthService.getHealth(); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(health); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthService.java deleted file mode 100644 index e00af08cf0..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthService.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.fairspace.saturn.services.health; - -import java.util.Collections; -import javax.sql.*; - -public class HealthService { - private final DataSource dataSource; - - public HealthService(DataSource dataSource) { - this.dataSource = dataSource; - } - - public Health getHealth() { - Health health = new Health(); - if (dataSource != null) { - HealthStatus dbStatus = getConnectionStatus(); - health.setStatus(dbStatus); - health.setComponents(Collections.singletonMap("viewDatabase", dbStatus)); - } - return health; - } - - private HealthStatus getConnectionStatus() { - final int connectionTimeout = 1; - try (var connection = dataSource.getConnection()) { - if (connection.isValid(connectionTimeout)) { - return HealthStatus.UP; - } - return HealthStatus.DOWN; - } catch (Exception e) { - return HealthStatus.DOWN; - } - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthStatus.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthStatus.java deleted file mode 100644 index b3713fe93e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/health/HealthStatus.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.fairspace.saturn.services.health; - -public enum HealthStatus { - UP, - DOWN -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java deleted file mode 100644 index c0fd85681e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceApp.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.fairspace.saturn.services.maintenance; - -import io.fairspace.saturn.services.BaseApp; - -import static javax.servlet.http.HttpServletResponse.*; -import static spark.Spark.get; -import static spark.Spark.post; - -public class MaintenanceApp extends BaseApp { - private final MaintenanceService maintenanceService; - - public MaintenanceApp(String basePath, MaintenanceService maintenanceService) { - super(basePath); - - this.maintenanceService = maintenanceService; - } - - @Override - protected void initApp() { - post("/reindex", (req, res) -> { - maintenanceService.startRecreateIndexTask(); - res.status(SC_NO_CONTENT); - return ""; - }); - post("/compact", (req, res) -> { - maintenanceService.compactRdfStorageTask(); - res.status(SC_NO_CONTENT); - return ""; - }); - get("/status", (req, res) -> { - res.status(SC_OK); - return maintenanceService.active() ? "active" : "inactive"; - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java index 21d5ba655d..e05c18b450 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/maintenance/MaintenanceService.java @@ -12,8 +12,12 @@ import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.tdb2.DatabaseMgr; import org.apache.jena.tdb2.store.DatasetGraphSwitchable; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; -import io.fairspace.saturn.config.ConfigLoader; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; import io.fairspace.saturn.rdf.transactions.TxnLogDatasetGraph; import io.fairspace.saturn.services.AccessDeniedException; @@ -25,27 +29,35 @@ import io.fairspace.saturn.services.views.ViewUpdater; @Log4j2 +@Service public class MaintenanceService { + public static final String SERVICE_NOT_AVAILABLE = "Service not available"; public static final String MAINTENANCE_IS_IN_PROGRESS = "Maintenance is in progress."; private final ThreadPoolExecutor threadpool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + private final ViewsProperties viewsProperties; private final UserService userService; private final Dataset dataset; private final ViewStoreClientFactory viewStoreClientFactory; private final ViewService viewService; + private final String publicUrl; public MaintenanceService( + ViewsProperties viewsProperties, @NonNull UserService userService, - @NonNull Dataset dataset, - ViewStoreClientFactory viewStoreClientFactory, - ViewService viewService) { + @Qualifier("dataset") @NonNull Dataset dataset, + @Nullable ViewStoreClientFactory viewStoreClientFactory, + ViewService viewService, + @Value("${application.publicUrl}") String publicUrl) { + this.viewsProperties = viewsProperties; this.userService = userService; this.dataset = dataset; this.viewStoreClientFactory = viewStoreClientFactory; this.viewService = viewService; + this.publicUrl = publicUrl; } public boolean disabled() { @@ -107,10 +119,11 @@ public void compactRdfStorageTask() { */ public void recreateIndex() { try (var viewStoreClient = viewStoreClientFactory.build(); - var viewUpdater = new ViewUpdater(viewStoreClient, dataset.asDatasetGraph())) { + var viewUpdater = + new ViewUpdater(viewsProperties, viewStoreClient, dataset.asDatasetGraph(), publicUrl)) { var start = new Date().getTime(); // Index entities - for (var view : ConfigLoader.VIEWS_CONFIG.views) { + for (var view : viewsProperties.views) { viewUpdater.recreateIndexForView(viewStoreClient, view); } viewUpdater.commit(); @@ -128,17 +141,12 @@ public void recreateIndex() { * @return the underlying dataset graph that supports compacting */ public static DatasetGraph unwrap(DatasetGraph dsg) { - if (dsg == null || dsg instanceof DatasetGraphSwitchable) { - return dsg; - } - if (dsg instanceof TxnLogDatasetGraph) { - return unwrap(((TxnLogDatasetGraph) dsg).getDatasetGraph()); - } - - if (dsg instanceof TxnIndexDatasetGraph) { - return unwrap(((TxnIndexDatasetGraph) dsg).getDatasetGraph()); - } - - return null; + return switch (dsg) { + case null -> dsg; + case DatasetGraphSwitchable ignored -> dsg; + case TxnLogDatasetGraph txnLogDatasetGraph -> unwrap(txnLogDatasetGraph.getDatasetGraph()); + case TxnIndexDatasetGraph txnIndexDatasetGraph -> unwrap(txnIndexDatasetGraph.getDatasetGraph()); + default -> null; + }; } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java deleted file mode 100644 index 35745ba07a..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataApp.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.fairspace.saturn.services.metadata; - -import lombok.extern.log4j.Log4j2; -import org.apache.jena.rdf.model.Model; -import spark.Request; - -import io.fairspace.saturn.services.AccessDeniedException; -import io.fairspace.saturn.services.BaseApp; -import io.fairspace.saturn.services.PayloadParsingException; -import io.fairspace.saturn.services.metadata.validation.ValidationException; - -import static io.fairspace.saturn.services.errors.ErrorHelper.errorBody; -import static io.fairspace.saturn.services.errors.ErrorHelper.exceptionHandler; -import static io.fairspace.saturn.services.metadata.Serialization.deserialize; -import static io.fairspace.saturn.services.metadata.Serialization.getFormat; -import static io.fairspace.saturn.services.metadata.Serialization.serialize; -import static io.fairspace.saturn.util.ValidationUtils.validate; -import static io.fairspace.saturn.util.ValidationUtils.validateIRI; - -import static java.lang.Boolean.TRUE; -import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static org.apache.jena.rdf.model.ResourceFactory.createResource; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.delete; -import static spark.Spark.get; -import static spark.Spark.patch; -import static spark.Spark.put; - -@Log4j2 -public class MetadataApp extends BaseApp { - - private static final String DO_VIEWS_UPDATE = "doViewsUpdate"; - - protected final MetadataService api; - - public MetadataApp(String basePath, MetadataService api) { - super(basePath); - this.api = api; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - var model = getMetadata(req); - var format = getFormat(req.headers("Accept")); - res.type(format.getLang().getHeaderString()); - return serialize(model, format); - }); - - put("/", (req, res) -> { - var model = deserialize(req.body(), req.contentType()); - var doMaterializedViewsRefresh = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - - api.put(model, Boolean.valueOf(doMaterializedViewsRefresh)); - - res.status(SC_NO_CONTENT); - return ""; - }); - patch("/", (req, res) -> { - var model = deserialize(req.body(), req.contentType()); - var doViewsUpdate = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - api.patch(model, Boolean.valueOf(doViewsUpdate)); - - res.status(SC_NO_CONTENT); - return ""; - }); - delete("/", (req, res) -> { - if (req.queryParams("subject") != null) { - var subject = req.queryParams("subject"); - validate(subject != null, "Parameter \"subject\" is required"); - validateIRI(subject); - if (!api.softDelete(createResource(subject))) { - // Subject could not be deleted. Return a 404 error response - return null; - } - } else { - var model = deserialize(req.body(), req.contentType()); - var doMaterializedViewsRefresh = req.queryParamOrDefault(DO_VIEWS_UPDATE, TRUE.toString()); - api.delete(model, Boolean.valueOf(doMaterializedViewsRefresh)); - } - - res.status(SC_NO_CONTENT); - return ""; - }); - exception(PayloadParsingException.class, exceptionHandler(SC_BAD_REQUEST, "Malformed request body")); - exception(ValidationException.class, (e, req, res) -> { - log.error("400 Error handling request {} {}", req.requestMethod(), req.uri()); - e.getViolations().forEach(v -> log.error("{}", v)); - - res.type(APPLICATION_JSON.asString()); - res.status(SC_BAD_REQUEST); - res.body(errorBody(SC_BAD_REQUEST, "Validation Error", e.getViolations())); - }); - exception(AccessDeniedException.class, (e, req, res) -> { - log.error("401 Access denied {} {} {}", e.getMessage(), req.requestMethod(), req.uri()); - - res.type(APPLICATION_JSON.asString()); - res.status(SC_FORBIDDEN); - res.body(errorBody(SC_FORBIDDEN, "Access denied", e.getMessage())); - }); - } - - private Model getMetadata(Request req) { - return api.get(req.queryParams("subject"), req.queryParams().contains("withValueProperties")); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataPermissions.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataPermissions.java index 27e6b381e8..88768382f9 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataPermissions.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataPermissions.java @@ -2,18 +2,27 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.services.workspaces.WorkspaceService; import io.fairspace.saturn.vocabulary.FS; import io.fairspace.saturn.webdav.DavFactory; +@Component public class MetadataPermissions { + private final WorkspaceService workspaceService; + private final DavFactory davFactory; + private final UserService userService; - public MetadataPermissions(WorkspaceService workspaceService, DavFactory davFactory, UserService userService) { + public MetadataPermissions( + WorkspaceService workspaceService, + @Qualifier("davFactory") DavFactory davFactory, + UserService userService) { this.workspaceService = workspaceService; this.davFactory = davFactory; this.userService = userService; @@ -53,4 +62,9 @@ public boolean canWriteMetadata(Resource resource) { } return userService.currentUser().isCanAddSharedMetadata(); } + + public boolean hasMetadataQueryPermission() { + var user = userService.currentUser(); + return user != null && user.isCanQueryMetadata(); + } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataService.java index edba7463b5..8888626f79 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/MetadataService.java @@ -14,6 +14,7 @@ import org.apache.jena.shacl.vocabulary.SHACLM; import org.apache.jena.util.iterator.ExtendedIterator; import org.apache.jena.vocabulary.RDF; +import org.springframework.beans.factory.annotation.Qualifier; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.services.AccessDeniedException; @@ -30,13 +31,13 @@ import static io.fairspace.saturn.rdf.SparqlUtils.toXSDDateTimeLiteral; import static io.fairspace.saturn.services.users.UserService.currentUserAsSymbol; import static io.fairspace.saturn.vocabulary.ShapeUtils.getPropertyShapesForResource; -import static io.fairspace.saturn.vocabulary.Vocabularies.SYSTEM_VOCABULARY; import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; public class MetadataService { private final Transactions transactions; private final Model vocabulary; + private final Model systemVocabulary; private final MetadataRequestValidator validator; private final MetadataPermissions permissions; @@ -46,11 +47,13 @@ public class MetadataService { public MetadataService( Transactions transactions, - Model vocabulary, + @Qualifier("vocabulary") Model vocabulary, + Model systemVocabulary, MetadataRequestValidator validator, MetadataPermissions permissions) { this.transactions = transactions; this.vocabulary = vocabulary; + this.systemVocabulary = systemVocabulary; this.validator = validator; this.permissions = permissions; } @@ -153,7 +156,7 @@ public boolean softDelete(Resource subject) { } var machineOnly = resource.listProperties(RDF.type) .mapWith(Statement::getObject) - .filterKeep(SYSTEM_VOCABULARY::containsResource) + .filterKeep(systemVocabulary::containsResource) .hasNext(); if (machineOnly) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java deleted file mode 100644 index 9585f46a1a..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/VocabularyApp.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.fairspace.saturn.services.metadata; - -import io.fairspace.saturn.services.BaseApp; - -import static io.fairspace.saturn.services.metadata.Serialization.getFormat; -import static io.fairspace.saturn.services.metadata.Serialization.serialize; -import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; - -import static spark.Spark.get; - -public class VocabularyApp extends BaseApp { - public VocabularyApp(String basePath) { - super(basePath); - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - var format = getFormat(req.headers("Accept")); - res.type(format.getLang().getHeaderString()); - return serialize(VOCABULARY, format); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ComposedValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ComposedValidator.java index 2baf9526c7..d3058e565e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ComposedValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ComposedValidator.java @@ -1,15 +1,20 @@ package io.fairspace.saturn.services.metadata.validation; +import java.util.List; + import org.apache.jena.graph.Node; import org.apache.jena.rdf.model.Model; +import org.springframework.stereotype.Component; /** * Combines a few validators into one. Stops on a first failing validator. */ +@Component("composedValidator") public class ComposedValidator implements MetadataRequestValidator { - private final MetadataRequestValidator[] validators; - public ComposedValidator(MetadataRequestValidator... validators) { + private final List validators; + + public ComposedValidator(List validators) { this.validators = validators; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/DeletionValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/DeletionValidator.java index abc4788afb..ab28f68fdd 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/DeletionValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/DeletionValidator.java @@ -2,9 +2,11 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.springframework.stereotype.Component; import io.fairspace.saturn.vocabulary.FS; +@Component public class DeletionValidator implements MetadataRequestValidator { @Override public void validate(Model before, Model after, Model removed, Model added, ViolationHandler violationHandler) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/MachineOnlyClassesValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/MachineOnlyClassesValidator.java index d42d8dd7db..10be5ed152 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/MachineOnlyClassesValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/MachineOnlyClassesValidator.java @@ -3,13 +3,16 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.shacl.vocabulary.SHACLM; import org.apache.jena.vocabulary.RDF; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; import io.fairspace.saturn.vocabulary.FS; import static io.fairspace.saturn.rdf.ModelUtils.getBooleanProperty; +@Component public class MachineOnlyClassesValidator extends VocabularyAwareValidator { - public MachineOnlyClassesValidator(Model vocabulary) { + public MachineOnlyClassesValidator(@Qualifier("vocabulary") Model vocabulary) { super(vocabulary); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidator.java index a62d44ee9f..c12fb9d3ab 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidator.java @@ -8,6 +8,8 @@ import org.apache.jena.rdf.model.Resource; import org.apache.jena.shacl.vocabulary.SHACLM; import org.apache.jena.vocabulary.RDF; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; import io.fairspace.saturn.vocabulary.FS; @@ -22,9 +24,10 @@ * This validator checks whether the requested action will modify any machine-only * predicates. If so, the request will not validate */ +@Component public class ProtectMachineOnlyPredicatesValidator extends VocabularyAwareValidator { - public ProtectMachineOnlyPredicatesValidator(Model vocabulary) { + public ProtectMachineOnlyPredicatesValidator(@Qualifier("vocabulary") Model vocabulary) { super(vocabulary); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ShaclValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ShaclValidator.java index 69102eb8e7..294f2aa1cc 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ShaclValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/ShaclValidator.java @@ -7,17 +7,18 @@ import org.apache.jena.shacl.vocabulary.SHACL; import org.apache.jena.sparql.path.P_Link; import org.apache.jena.sparql.path.Path; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; import static org.apache.jena.shacl.validation.ValidationProc.plainValidationNode; +@Component public class ShaclValidator extends VocabularyAwareValidator { private final Shapes shapes; - public ShaclValidator(Model vocabulary) { + public ShaclValidator(@Qualifier("vocabulary") Model vocabulary) { super(vocabulary); - shapes = Shapes.parse(vocabulary); - ; } @Override diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidator.java index 43f814b948..02e09896ef 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidator.java @@ -2,12 +2,18 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import static io.fairspace.saturn.config.WebDAVConfig.WEB_DAV_URL_PATH; + +@Component public class URIPrefixValidator implements MetadataRequestValidator { + private final String restrictedPrefix; - public URIPrefixValidator(String restrictedPrefix) { - this.restrictedPrefix = restrictedPrefix; + public URIPrefixValidator(@Value("${application.publicUrl}") String publicUrl) { + this.restrictedPrefix = publicUrl + WEB_DAV_URL_PATH; // should be the same as dav root unique id } @Override diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/UniqueLabelValidator.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/UniqueLabelValidator.java index be98269c6b..859673e392 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/UniqueLabelValidator.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/metadata/validation/UniqueLabelValidator.java @@ -3,9 +3,11 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import org.springframework.stereotype.Component; import io.fairspace.saturn.vocabulary.FS; +@Component public class UniqueLabelValidator implements MetadataRequestValidator { @Override public void validate(Model before, Model after, Model removed, Model added, ViolationHandler violationHandler) { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java index 818cf2ad1c..2baa761878 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/FileSearchService.java @@ -2,6 +2,9 @@ import java.util.List; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; + public interface FileSearchService { - List searchFiles(FileSearchRequest request); + List searchFiles(FileSearchRequest request); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java index f625d82b5c..c7a9648872 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/JdbcFileSearchService.java @@ -7,43 +7,33 @@ import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.transactions.Transactions; -import io.fairspace.saturn.services.views.ViewStoreClientFactory; import io.fairspace.saturn.services.views.ViewStoreReader; import static io.fairspace.saturn.webdav.PathUtils.getCollectionNameByUri; @Log4j2 public class JdbcFileSearchService implements FileSearchService { + private final Transactions transactions; private final CollectionResource rootSubject; - private final Config.Search searchConfig; - private final ViewsConfig viewsConfig; - private final ViewStoreClientFactory viewStoreClientFactory; + private final ViewStoreReader viewStoreReader; public JdbcFileSearchService( - Config.Search searchConfig, - ViewsConfig viewsConfig, - ViewStoreClientFactory viewStoreClientFactory, - Transactions transactions, - CollectionResource rootSubject) { - this.searchConfig = searchConfig; - this.viewStoreClientFactory = viewStoreClientFactory; + Transactions transactions, CollectionResource rootSubject, ViewStoreReader viewStoreReader) { this.transactions = transactions; this.rootSubject = rootSubject; - this.viewsConfig = viewsConfig; + this.viewStoreReader = viewStoreReader; } @SneakyThrows - public List searchFiles(FileSearchRequest request) { + public List searchFiles(FileSearchRequest request) { var collectionsForUser = transactions.calculateRead(m -> rootSubject.getChildren().stream() .map(collection -> getCollectionNameByUri(rootSubject.getUniqueId(), collection.getUniqueId())) .collect(Collectors.toList())); - try (var viewStoreReader = new ViewStoreReader(searchConfig, viewsConfig, viewStoreClientFactory)) { - return viewStoreReader.searchFiles(request, collectionsForUser); - } + return viewStoreReader.searchFiles(request, collectionsForUser); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java deleted file mode 100644 index 1b7030d9d7..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchApp.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.fairspace.saturn.services.search; - -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.post; - -public class SearchApp extends BaseApp { - private final SearchService searchService; - private final FileSearchService fileSearchService; - - public SearchApp(String basePath, SearchService searchService, FileSearchService fileSearchService) { - super(basePath); - this.searchService = searchService; - this.fileSearchService = fileSearchService; - } - - @Override - protected void initApp() { - post("/files", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var request = mapper.readValue(req.body(), FileSearchRequest.class); - var searchResult = fileSearchService.searchFiles(request); - - SearchResultsDTO resultDto = SearchResultsDTO.builder() - .results(searchResult) - .query(request.getQuery()) - .build(); - - return mapper.writeValueAsString(resultDto); - }); - - post("/lookup", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var request = mapper.readValue(req.body(), LookupSearchRequest.class); - var results = searchService.getLookupSearchResults(request); - return mapper.writeValueAsString(results); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java deleted file mode 100644 index e8bdcfe426..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.fairspace.saturn.services.search; - -import lombok.Builder; -import lombok.NonNull; -import lombok.Value; - -@Value -@Builder -public class SearchResultDTO { - @NonNull - String id; - - String label; - String type; - String comment; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java deleted file mode 100644 index 686789ad71..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchResultsDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.fairspace.saturn.services.search; - -import java.util.List; - -import lombok.Builder; -import lombok.Value; - -@Value -@Builder -public class SearchResultsDTO { - List results; - String query; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java index 5d525233a6..35074a4b44 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SearchService.java @@ -2,9 +2,17 @@ import java.util.List; -import lombok.extern.log4j.*; -import org.apache.jena.query.*; +import lombok.extern.log4j.Log4j2; +import org.apache.jena.query.Dataset; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.QuerySolutionMap; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.vocabulary.FS; @@ -12,6 +20,7 @@ import static org.apache.jena.rdf.model.ResourceFactory.createStringLiteral; @Log4j2 +@Service public class SearchService { private static final Query RESOURCE_BY_TEXT_QUERY = QueryFactory.create(String.format( """ @@ -44,30 +53,30 @@ public class SearchService { """, FS.NS)); - private final Dataset ds; + private final Dataset filteredDataset; - public SearchService(Dataset ds) { - this.ds = ds; + public SearchService(@Qualifier("filteredDataset") Dataset filteredDataset) { + this.filteredDataset = filteredDataset; } - public SearchResultsDTO getLookupSearchResults(LookupSearchRequest request) { - return SearchResultsDTO.builder() + public SearchResultsDto getLookupSearchResults(LookupSearchRequest request) { + return SearchResultsDto.builder() .results(getResourceByText(request)) .query(request.getQuery()) .build(); } - private List getResourceByText(LookupSearchRequest request) { + private List getResourceByText(LookupSearchRequest request) { var binding = new QuerySolutionMap(); binding.add("query", createStringLiteral(request.getQuery())); binding.add("type", createResource(request.getResourceType())); - var results = SparqlUtils.getByQuery(RESOURCE_BY_TEXT_EXACT_MATCH_QUERY, binding, ds); - if (results.size() > 0) { + var results = SparqlUtils.getByQuery(RESOURCE_BY_TEXT_EXACT_MATCH_QUERY, binding, filteredDataset); + if (!results.isEmpty()) { return results; } binding.add("regexQuery", createStringLiteral(SparqlUtils.getQueryRegex(request.getQuery()))); - return SparqlUtils.getByQuery(RESOURCE_BY_TEXT_QUERY, binding, ds); + return SparqlUtils.getByQuery(RESOURCE_BY_TEXT_QUERY, binding, filteredDataset); } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java index a38ada61e6..3c05a16b4a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/search/SparqlFileSearchService.java @@ -8,6 +8,8 @@ import org.apache.jena.query.QueryFactory; import org.apache.jena.query.QuerySolutionMap; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.vocabulary.FS; @@ -23,7 +25,7 @@ public SparqlFileSearchService(Dataset ds) { this.ds = ds; } - public List searchFiles(FileSearchRequest request) { + public List searchFiles(FileSearchRequest request) { var query = getSearchForFilesQuery(request.getParentIRI()); var binding = new QuerySolutionMap(); binding.add("regexQuery", createStringLiteral(SparqlUtils.getQueryRegex(request.getQuery()))); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/LogoutApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/users/LogoutApp.java deleted file mode 100644 index 750be50262..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/LogoutApp.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.fairspace.saturn.services.users; - -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.services.BaseApp; - -import static io.fairspace.saturn.auth.RequestContext.getIdTokenString; - -import static javax.servlet.http.HttpServletResponse.SC_SEE_OTHER; -import static spark.Spark.get; - -public class LogoutApp extends BaseApp { - private final UserService service; - private final Config config; - - public LogoutApp(String basePath, UserService service, Config config) { - super(basePath); - this.service = service; - this.config = config; - } - - @Override - protected void initApp() { - get("", (req, res) -> { - var idToken = getIdTokenString(); - service.logoutCurrent(); - res.status(SC_SEE_OTHER); - res.header( - "Location", - "%srealms/%s/protocol/openid-connect/logout?post_logout_redirect_uri=%s&id_token_hint=%s" - .formatted(config.auth.authServerUrl, config.auth.realm, config.publicUrl, idToken)); - return ""; - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java deleted file mode 100644 index f02dce285b..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserApp.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.fairspace.saturn.services.users; - -import io.fairspace.saturn.services.BaseApp; - -import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.*; - -public class UserApp extends BaseApp { - private final UserService service; - - public UserApp(String basePath, UserService service) { - super(basePath); - this.service = service; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(service.getUsers()); - }); - - patch("/", (req, res) -> { - service.update(mapper.readValue(req.body(), UserRolesUpdate.class)); - res.status(SC_NO_CONTENT); - return ""; - }); - - get("/current", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - var user = service.currentUser(); - return mapper.writeValueAsString(user); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserService.java index 94a324dd1b..3a747a9492 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/users/UserService.java @@ -1,85 +1,70 @@ package io.fairspace.saturn.services.users; -import java.util.*; -import java.util.concurrent.*; -import java.util.stream.*; -import javax.servlet.ServletException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import jakarta.ws.rs.NotFoundException; -import lombok.extern.log4j.*; -import org.apache.commons.lang3.*; -import org.apache.jena.graph.Node; +import lombok.extern.log4j.Log4j2; +import org.apache.commons.lang3.StringUtils; import org.apache.jena.sparql.util.Symbol; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.admin.client.resource.UsersResource; +import org.springframework.stereotype.Service; -import io.fairspace.saturn.auth.RequestContext; -import io.fairspace.saturn.config.Config; +import io.fairspace.saturn.config.properties.KeycloakClientProperties; +import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.rdf.dao.DAO; -import io.fairspace.saturn.rdf.dao.PersistentEntity; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.services.AccessDeniedException; import static io.fairspace.saturn.audit.Audit.audit; -import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; -import static io.fairspace.saturn.auth.RequestContext.getUserURI; -import static io.fairspace.saturn.rdf.SparqlUtils.generateMetadataIri; +import static io.fairspace.saturn.auth.RequestContext.getCurrentUserStringUri; -import static java.lang.System.getenv; import static java.util.stream.Collectors.toMap; @Log4j2 +@Service public class UserService { - private final LoadingCache> usersCache; + private final LoadingCache> usersCache; private final Transactions transactions; - private final Config.Auth config; + private final KeycloakClientProperties keycloakClientProperties; private final UsersResource usersResource; private final ExecutorService threadpool = Executors.newSingleThreadExecutor(); - public UserService(Config.Auth config, Transactions transactions, UsersResource usersResource) { - this.config = config; + public UserService( + KeycloakClientProperties keycloakClientProperties, Transactions transactions, UsersResource usersResource) { + this.keycloakClientProperties = keycloakClientProperties; this.transactions = transactions; this.usersResource = usersResource; usersCache = CacheBuilder.newBuilder() .refreshAfterWrite(30, TimeUnit.SECONDS) .build(new CacheLoader<>() { @Override - public Map load(Boolean key) { - return fetchUsers(); + public Map load(Boolean key) { + return fetchAndUpdateUsers(); } }); } - public UserService(Config.Auth config, Transactions transactions) { - this( - config, - transactions, - KeycloakBuilder.builder() - .serverUrl(config.authServerUrl) - .realm(config.realm) - .grantType(OAuth2Constants.CLIENT_CREDENTIALS) - .clientId(config.clientId) - .clientSecret(getenv("KEYCLOAK_CLIENT_SECRET")) - .username(config.clientId) - .password(getenv("KEYCLOAK_CLIENT_SECRET")) - .build() - .realm(config.realm) - .users()); - } - public Collection getUsers() { return getUsersMap().values(); } public User currentUser() { - return getUsersMap().get(getUserURI()); + return getCurrentUserStringUri().map(uri -> getUsersMap().get(uri)).orElse(null); } - public Map getUsersMap() { + public Map getUsersMap() { try { return usersCache.get(Boolean.FALSE); } catch (ExecutionException e) { @@ -88,11 +73,15 @@ public Map getUsersMap() { } public static Symbol currentUserAsSymbol() { - var uri = RequestContext.getCurrentUserStringUri().orElse("anonymous"); + var uri = getCurrentUserStringUri().orElse("anonymous"); return Symbol.create(uri); } - private Map fetchUsers() { + /** + * Fetches users from Keycloak and updates the users in the database + * if a user does not exist or if the user details have changed. + */ + private Map fetchAndUpdateUsers() { var userCount = usersResource.count(); var keycloakUsers = usersResource.list(0, userCount); var updated = new HashSet(); @@ -100,21 +89,21 @@ private Map fetchUsers() { var dao = new DAO(model); return keycloakUsers.stream() .map(ku -> { - var iri = generateMetadataIri(ku.getId()); + var iri = SparqlUtils.generateMetadataIriFromId(ku.getId()); var user = dao.read(User.class, iri); if (user == null) { user = new User(); user.setIri(iri); user.setId(ku.getId()); - if (config.superAdminUser.equalsIgnoreCase(ku.getUsername())) { + if (keycloakClientProperties.getSuperAdminUser().equalsIgnoreCase(ku.getUsername())) { user.setSuperadmin(true); user.setAdmin(true); user.setCanViewPublicMetadata(true); user.setCanViewPublicData(true); } - for (var role : config.defaultUserRoles) { + for (var role : keycloakClientProperties.getDefaultUserRoles()) { switch (role) { case "admin" -> user.setAdmin(true); case "canViewPublicMetadata" -> user.setCanViewPublicMetadata(true); @@ -146,7 +135,7 @@ private Map fetchUsers() { return user; }) - .collect(toMap(PersistentEntity::getIri, u -> u)); + .collect(toMap(user -> user.getIri().toString(), u -> u)); }); if (!updated.isEmpty()) { @@ -161,14 +150,6 @@ private Map fetchUsers() { return users; } - public void logoutCurrent() { - try { - getCurrentRequest().logout(); - } catch (ServletException e) { - throw new RuntimeException(e); - } - } - public void update(UserRolesUpdate roles) { if (!currentUser().isAdmin()) { throw new AccessDeniedException(); @@ -176,7 +157,7 @@ public void update(UserRolesUpdate roles) { final String[] username = new String[1]; transactions.executeWrite(model -> { var dao = new DAO(model); - var user = dao.read(User.class, generateMetadataIri(roles.getId())); + var user = dao.read(User.class, SparqlUtils.generateMetadataIriFromId(roles.getId())); if (user == null) { throw new NotFoundException(); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java deleted file mode 100644 index f79f1efd4d..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ColumnDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Value; - -import io.fairspace.saturn.config.*; - -@Value -public class ColumnDTO { - String name; - String title; - ViewsConfig.ColumnType type; - Integer displayIndex; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java deleted file mode 100644 index 89fac90c6f..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/CountDTO.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Data; - -@Data -public class CountDTO { - private final long count; - private final boolean timeout; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java deleted file mode 100644 index 80afb37f68..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetDTO.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import com.fasterxml.jackson.annotation.*; -import lombok.Value; - -import io.fairspace.saturn.config.*; - -import static com.fasterxml.jackson.annotation.JsonInclude.Include.*; - -@Value -@JsonInclude(NON_NULL) -public class FacetDTO { - String name; - String title; - ViewsConfig.ColumnType type; - List values; - Boolean booleanValue; - Object min; - Object max; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java deleted file mode 100644 index 358ba6eb9a..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/FacetsDTO.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import lombok.Value; - -@Value -public class FacetsDTO { - List facets; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java index bfc2629a13..9373fe07cb 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/JdbcQueryService.java @@ -1,16 +1,22 @@ package io.fairspace.saturn.services.views; -import java.sql.SQLException; import java.sql.SQLTimeoutException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import io.milton.resource.CollectionResource; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; @@ -27,27 +33,16 @@ */ @Log4j2 public class JdbcQueryService implements QueryService { + private final Transactions transactions; private final CollectionResource rootSubject; - private final Config.Search searchConfig; - private final ViewsConfig viewsConfig; - private final ViewStoreClientFactory viewStoreClientFactory; + private final ViewStoreReader viewStoreReader; public JdbcQueryService( - Config.Search searchConfig, - ViewsConfig viewsConfig, - ViewStoreClientFactory viewStoreClientFactory, - Transactions transactions, - CollectionResource rootSubject) { - this.searchConfig = searchConfig; - this.viewStoreClientFactory = viewStoreClientFactory; + Transactions transactions, CollectionResource rootSubject, ViewStoreReader viewStoreReader) { this.transactions = transactions; this.rootSubject = rootSubject; - this.viewsConfig = viewsConfig; - } - - ViewStoreReader getViewStoreReader() throws SQLException { - return new ViewStoreReader(searchConfig, viewsConfig, viewStoreClientFactory); + this.viewStoreReader = viewStoreReader; } @SneakyThrows @@ -79,7 +74,7 @@ protected void applyCollectionsFilterIfRequired(String view, List fi } @SneakyThrows - public ViewPageDTO retrieveViewPage(ViewRequest request) { + public ViewPageDto retrieveViewPage(ViewRequest request) { int page = (request.getPage() != null && request.getPage() >= 1) ? request.getPage() : 1; int size = (request.getSize() != null && request.getSize() >= 1) ? request.getSize() : 20; var filters = new ArrayList(); @@ -87,10 +82,10 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { filters.addAll(request.getFilters()); } applyCollectionsFilterIfRequired(request.getView(), filters); - try (var viewStoreReader = getViewStoreReader()) { - List>> rows = viewStoreReader.retrieveRows( + try { + List>> rows = viewStoreReader.retrieveRows( request.getView(), filters, (page - 1) * size, size + 1, request.includeJoinedViews()); - var pageBuilder = ViewPageDTO.builder() + var pageBuilder = ViewPageDto.builder() .rows(rows.subList(0, min(size, rows.size()))) .hasNext(rows.size() > size); if (request.includeCounts()) { @@ -99,7 +94,7 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } return pageBuilder.build(); } catch (SQLTimeoutException e) { - return ViewPageDTO.builder() + return ViewPageDto.builder() .rows(Collections.emptyList()) .timeout(true) .build(); @@ -107,16 +102,16 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { } @SneakyThrows - public CountDTO count(CountRequest request) { + public CountDto count(CountRequest request) { var filters = request.getFilters(); if (filters == null) { filters = new ArrayList<>(); } applyCollectionsFilterIfRequired(request.getView(), filters); - try (var viewStoreReader = getViewStoreReader()) { - return new CountDTO(viewStoreReader.countRows(request.getView(), filters), false); + try { + return new CountDto(viewStoreReader.countRows(request.getView(), filters), false); } catch (SQLTimeoutException e) { - return new CountDTO(0, true); + return new CountDto(0, true); } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java index c4bba535a9..3eacc95b06 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/MaterializedViewService.java @@ -9,12 +9,13 @@ import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; -import io.fairspace.saturn.config.ViewsConfig; - -import static io.fairspace.saturn.config.ConfigLoader.VIEWS_CONFIG; +import io.fairspace.saturn.config.properties.ViewsProperties; @Slf4j +@Service public class MaterializedViewService { private static final String INDEX_POSTFIX = "_idx"; @@ -23,18 +24,23 @@ public class MaterializedViewService { private final DataSource dataSource; private final ViewStoreClient.ViewStoreConfiguration configuration; + private final ViewsProperties viewsProperties; private final int maxJoinItems; public MaterializedViewService( - DataSource dataSource, ViewStoreClient.ViewStoreConfiguration configuration, int maxJoinItems) { + DataSource dataSource, + ViewStoreClient.ViewStoreConfiguration configuration, + ViewsProperties viewsProperties, + @Value("${application.search.maxJoinItems}") int maxJoinItems) { this.dataSource = dataSource; this.configuration = configuration; + this.viewsProperties = viewsProperties; this.maxJoinItems = maxJoinItems; } public void createOrUpdateAllMaterializedViews() { try (var connection = dataSource.getConnection()) { - for (var view : VIEWS_CONFIG.views) { + for (var view : viewsProperties.views) { createOrUpdateViewMaterializedView(view, connection); createOrUpdateJoinMaterializedView(view, connection); } @@ -44,7 +50,8 @@ public void createOrUpdateAllMaterializedViews() { } } - private void createOrUpdateViewMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { + private void createOrUpdateViewMaterializedView(ViewsProperties.View view, Connection connection) + throws SQLException { var setColumns = view.columns.stream().filter(column -> column.type.isSet()).toList(); if (!setColumns.isEmpty()) { @@ -78,8 +85,9 @@ private void createOrUpdateViewMaterializedView(ViewsConfig.View view, Connectio } } - private void createOrUpdateJoinMaterializedView(ViewsConfig.View view, Connection connection) throws SQLException { - for (ViewsConfig.View.JoinView joinView : view.join) { + private void createOrUpdateJoinMaterializedView(ViewsProperties.View view, Connection connection) + throws SQLException { + for (ViewsProperties.View.JoinView joinView : view.join) { String viewName = view.name.toLowerCase(); String joinViewName = joinView.view.toLowerCase(); var mvName = "mv_%s_join_%s".formatted(viewName, joinViewName); @@ -153,8 +161,8 @@ private boolean doesIndexExist(String mvName, String idxName, Connection connect private void createViewMaterializedView( String viewOrTableName, - ViewsConfig.View view, - List setColumns, + ViewsProperties.View view, + List setColumns, Connection connection) throws SQLException { String viewName = view.name.toLowerCase(); @@ -210,7 +218,8 @@ private void createMaterializedViewIndex( } private void createJoinMaterializedViews( - ViewsConfig.View view, ViewsConfig.View.JoinView joinView, Connection connection) throws SQLException { + ViewsProperties.View view, ViewsProperties.View.JoinView joinView, Connection connection) + throws SQLException { var viewTableName = view.name.toLowerCase(); var joinTable = configuration.joinTables.get(view.name).get(joinView.view).name; var joinedTable = configuration.viewTables.get(joinView.view).name.toLowerCase(); @@ -320,7 +329,7 @@ private void createJoinMaterializedViews( } } - private List collectViewColumns(ViewsConfig.View view) { + private List collectViewColumns(ViewsProperties.View view) { List columns = view.columns.stream() .filter(column -> column.type.isSet()) .map(column -> column.name.toLowerCase()) @@ -330,7 +339,7 @@ private List collectViewColumns(ViewsConfig.View view) { return columns; } - private List collectJoinColumns(ViewsConfig.View view, ViewsConfig.View.JoinView joinView) { + private List collectJoinColumns(ViewsProperties.View view, ViewsProperties.View.JoinView joinView) { var viewTableName = view.name.toLowerCase(); var joinedTable = configuration.viewTables.get(joinView.view).name.toLowerCase(); var viewIdColumn = viewTableName + "_id"; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java index eb49521400..a1f3294d72 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/QueryService.java @@ -1,5 +1,10 @@ package io.fairspace.saturn.services.views; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; + /** * High-level interface for fetching metadata view pages and counts. * Implemented using Sparql queries on the RDF database directly @@ -14,7 +19,7 @@ * collections the user has access to. */ public interface QueryService { - ViewPageDTO retrieveViewPage(ViewRequest request); + ViewPageDto retrieveViewPage(ViewRequest request); - CountDTO count(CountRequest request); + CountDto count(CountRequest request); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java index dd669e2436..55876b2ad1 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/SparqlQueryService.java @@ -1,5 +1,6 @@ package io.fairspace.saturn.services.views; +import java.io.ByteArrayOutputStream; import java.time.Instant; import java.util.ArrayList; import java.util.Calendar; @@ -8,14 +9,18 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.TimeUnit; import lombok.extern.log4j.Log4j2; import org.apache.jena.datatypes.xsd.XSDDateTime; import org.apache.jena.query.Dataset; import org.apache.jena.query.Query; import org.apache.jena.query.QueryCancelledException; -import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.ResultSetFormatter; +import org.apache.jena.query.Syntax; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.Statement; @@ -32,11 +37,20 @@ import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.syntax.ElementFilter; import org.apache.jena.vocabulary.RDFS; - -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; -import io.fairspace.saturn.config.ViewsConfig.ColumnType; -import io.fairspace.saturn.config.ViewsConfig.View; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; + +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties.ColumnType; +import io.fairspace.saturn.config.properties.ViewsProperties.View; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; +import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.vocabulary.FS; import static io.fairspace.saturn.rdf.ModelUtils.getResourceProperties; @@ -58,19 +72,54 @@ import static org.apache.jena.system.Txn.calculateRead; @Log4j2 +@Service +@Qualifier("sparqlQueryService") public class SparqlQueryService implements QueryService { + private static final String RESOURCES_VIEW = "Resource"; - private final Config.Search config; - private final ViewsConfig viewsConfig; - private final Dataset ds; - public SparqlQueryService(Config.Search config, ViewsConfig viewsConfig, Dataset ds) { - this.config = config; - this.viewsConfig = viewsConfig; + private final SearchProperties searchProperties; + private final JenaProperties jenaProperties; + private final ViewsProperties viewsProperties; + private final Dataset ds; + private final Transactions transactions; + + public SparqlQueryService( + SearchProperties searchProperties, + JenaProperties jenaProperties, + ViewsProperties viewsProperties, + @Qualifier("filteredDataset") Dataset ds, + Transactions transactions) { + this.searchProperties = searchProperties; + this.jenaProperties = jenaProperties; + this.viewsProperties = viewsProperties; this.ds = ds; + this.transactions = transactions; + } + + /** + * Execute a SPARQL query and return the results as a JSON string. + */ + public String executeQuery(String sparqlQuery) { + return transactions.calculateRead(model -> { + Query query = QueryFactory.create(sparqlQuery, Syntax.syntaxARQ); + try (QueryExecution queryExecution = QueryExecution.create() + .query(query) + .dataset(ds) + .timeout(jenaProperties.getSparqlQueryTimeout(), TimeUnit.MILLISECONDS) + .build()) { + ResultSet resultSet = queryExecution.execSelect(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ResultSetFormatter.outputAsJSON(outputStream, resultSet); + return outputStream.toString(); + } catch (Exception e) { + log.error("Error executing query: \n{}", sparqlQuery, e); + throw new RuntimeException(e); + } + }); } - public ViewPageDTO retrieveViewPage(ViewRequest request) { + public ViewPageDto retrieveViewPage(ViewRequest request) { var query = getQuery(request, false); log.debug("Executing query:\n{}", query); @@ -82,40 +131,43 @@ public ViewPageDTO retrieveViewPage(ViewRequest request) { log.debug("Query with filters and pagination applied: \n{}", query); - var selectExecution = QueryExecutionFactory.create(query, ds); - selectExecution.setTimeout(config.pageRequestTimeout); - - return calculateRead(ds, () -> { - var iris = new ArrayList(); - var timeout = false; - var hasNext = false; - try (selectExecution) { - var rs = selectExecution.execSelect(); - rs.forEachRemaining(row -> iris.add(row.getResource(request.getView()))); - } catch (QueryCancelledException e) { - timeout = true; - } - while (iris.size() > size) { - iris.remove(iris.size() - 1); - hasNext = true; - } + try (var selectExecution = QueryExecution.create() + .dataset(ds) + .query(query) + .timeout(searchProperties.getCountRequestTimeout()) + .build()) { + return calculateRead(ds, () -> { + var iris = new ArrayList(); + var timeout = false; + var hasNext = false; + try (selectExecution) { + var rs = selectExecution.execSelect(); + rs.forEachRemaining(row -> iris.add(row.getResource(request.getView()))); + } catch (QueryCancelledException e) { + timeout = true; + } + while (iris.size() > size) { + iris.remove(iris.size() - 1); + hasNext = true; + } - var rows = iris.stream() - .map(resource -> fetch(resource, request.getView())) - .collect(toList()); + var rows = iris.stream() + .map(resource -> fetch(resource, request.getView())) + .collect(toList()); - return ViewPageDTO.builder() - .rows(rows) - .hasNext(hasNext) - .timeout(timeout) - .build(); - }); + return ViewPageDto.builder() + .rows(rows) + .hasNext(hasNext) + .timeout(timeout) + .build(); + }); + } } - private Map> fetch(Resource resource, String viewName) { + private Map> fetch(Resource resource, String viewName) { var view = getView(viewName); - var result = new HashMap>(); + var result = new HashMap>(); result.put(view.name, Set.of(toValueDTO(resource))); for (var c : view.columns) { @@ -153,7 +205,7 @@ private Map> fetch(Resource resource, String viewName) { return result; } - private Set getValues(Resource resource, View.Column column) { + private Set getValues(Resource resource, View.Column column) { return new TreeSet<>(resource.listProperties(createProperty(column.source)) .mapWith(Statement::getObject) .mapWith(this::toValueDTO) @@ -161,18 +213,18 @@ private Set getValues(Resource resource, View.Column column) { } private View getView(String viewName) { - return viewsConfig + return viewsProperties .getViewConfig(viewName) .orElseThrow(() -> new IllegalArgumentException("Unknown view: " + viewName)); } - private ValueDTO toValueDTO(RDFNode node) { + private ValueDto toValueDTO(RDFNode node) { if (node.isLiteral()) { var value = node.asLiteral().getValue(); if (value instanceof XSDDateTime) { value = ofEpochMilli(((XSDDateTime) value).asCalendar().getTimeInMillis()); } - return new ValueDTO(value.toString(), value); + return new ValueDto(value.toString(), value); } var resource = node.asResource(); var label = resource.listProperties(RDFS.label) @@ -180,7 +232,7 @@ private ValueDTO toValueDTO(RDFNode node) { .map(Statement::getString) .orElseGet(resource::getLocalName); - return new ValueDTO(label, resource.getURI()); + return new ValueDto(label, resource.getURI()); } private Query getQuery(CountRequest request, boolean isCount) { @@ -394,26 +446,29 @@ private static boolean convertBooleanValue(String value) { return Boolean.getBoolean(value); } - public CountDTO count(CountRequest request) { + public CountDto count(CountRequest request) { var query = getQuery(request, true); log.debug("Querying the total number of matches: \n{}", query); - try (var execution = QueryExecutionFactory.create(query, ds)) { - execution.setTimeout(config.countRequestTimeout); + try (var execution = QueryExecution.create() + .dataset(ds) + .query(query) + .timeout(searchProperties.getCountRequestTimeout()) + .build()) { return calculateRead(ds, () -> { var queryResult = execution.execSelect(); if (queryResult.hasNext()) { var row = queryResult.next(); var count = row.getLiteral("count").getLong(); - return new CountDTO(count, false); + return new CountDto(count, false); } else { - return new CountDTO(0, false); + return new CountDto(0, false); } }); } catch (QueryCancelledException e) { - return new CountDTO(0, true); + return new CountDto(0, true); } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/Table.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/Table.java index fe72877190..2d6ad5b750 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/Table.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/Table.java @@ -6,6 +6,8 @@ import lombok.*; +import io.fairspace.saturn.config.properties.ViewsProperties; + import static io.fairspace.saturn.config.ViewsConfig.*; @Data @@ -14,7 +16,7 @@ public class Table { @Builder public static class ColumnDefinition { String name; - ColumnType type; + ViewsProperties.ColumnType type; } public static ColumnDefinition idColumn() { @@ -24,11 +26,11 @@ public static ColumnDefinition idColumn() { public static ColumnDefinition idColumn(String prefix) { return ColumnDefinition.builder() .name(prefix == null ? "id" : prefix.toLowerCase() + "_id") - .type(ColumnType.Identifier) + .type(ViewsProperties.ColumnType.Identifier) .build(); } - public static ColumnDefinition valueColumn(String name, ColumnType type) { + public static ColumnDefinition valueColumn(String name, ViewsProperties.ColumnType type) { return ColumnDefinition.builder().name(name.toLowerCase()).type(type).build(); } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java deleted file mode 100644 index c355f2a6ad..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ValueDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.Value; - -@Value -public class ValueDTO implements Comparable { - String label; - Object value; - - @Override - public int compareTo(ValueDTO o) { - return label.compareTo(o.label); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java deleted file mode 100644 index 4c9a564cd4..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewApp.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.fairspace.saturn.services.views; - -import lombok.extern.slf4j.Slf4j; - -import io.fairspace.saturn.services.BaseApp; - -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.get; -import static spark.Spark.post; - -@Slf4j -public class ViewApp extends BaseApp { - - private final ViewService viewService; - private final QueryService queryService; - - public ViewApp(String basePath, ViewService viewService, QueryService queryService) { - super(basePath); - this.viewService = viewService; - this.queryService = queryService; - } - - @Override - protected void initApp() { - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(new ViewsDTO(viewService.getViews())); - }); - - post("/", (req, res) -> { - var requestBody = mapper.readValue(req.body(), ViewRequest.class); - var result = queryService.retrieveViewPage(requestBody); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(result); - }); - - get("/facets", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(new FacetsDTO(viewService.getFacets())); - }); - - post("/count", (req, res) -> { - var result = queryService.count(mapper.readValue(req.body(), CountRequest.class)); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(result); - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java deleted file mode 100644 index 6ba67f863e..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Value; - -@Value -public class ViewDTO { - String name; - String title; - List columns; - - @JsonInclude(JsonInclude.Include.NON_NULL) - Long maxDisplayCount; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java index a5502031d0..791ec3e0c6 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewRow.java @@ -9,25 +9,27 @@ import com.google.common.collect.Sets; +import io.fairspace.saturn.controller.dto.ValueDto; + public class ViewRow { - private final Map> data; + private final Map> data; public ViewRow() { this.data = new HashMap<>(); } - public ViewRow(Map> data) { + public ViewRow(Map> data) { this.data = data; } public static ViewRow viewSetOf(ResultSet resultSet, List columnsNames, String viewName) throws SQLException { - var data = new HashMap>(); + var data = new HashMap>(); for (String columnName : columnsNames) { String label = resultSet.getString(columnName); var key = viewName + "_" + columnName; - var value = Sets.newHashSet(new ValueDTO(label, label)); + var value = Sets.newHashSet(new ValueDto(label, label)); data.put(key, value); } return new ViewRow(data); @@ -35,11 +37,11 @@ public static ViewRow viewSetOf(ResultSet resultSet, List columnsNames, // TODO, make obsolete by ViewStoreReader refactor // TODO: return unmodifiable map - public Map> getRawData() { + public Map> getRawData() { return data; } - public void put(String key, Set value) { + public void put(String key, Set value) { data.put(key, value); } @@ -48,7 +50,7 @@ public ViewRow merge(ViewRow anotherViewRow) { return this; } - private static Set addElementsAndReturn(Set set, Set elements) { + private static Set addElementsAndReturn(Set set, Set elements) { set.addAll(elements); return set; } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java index ef156dad1b..172e6b340a 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewService.java @@ -20,16 +20,24 @@ import org.apache.jena.query.QuerySolution; import org.apache.jena.query.QuerySolutionMap; import org.apache.jena.rdf.model.Literal; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.config.properties.CacheProperties; +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.controller.dto.ColumnDto; +import io.fairspace.saturn.controller.dto.FacetDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewDto; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; import io.fairspace.saturn.services.AccessDeniedException; import io.fairspace.saturn.services.metadata.MetadataPermissions; import io.fairspace.saturn.vocabulary.FS; -import static io.fairspace.saturn.config.ViewsConfig.ColumnType; -import static io.fairspace.saturn.config.ViewsConfig.View; +import static io.fairspace.saturn.config.properties.ViewsProperties.ColumnType; +import static io.fairspace.saturn.config.properties.ViewsProperties.View; import static java.time.Instant.ofEpochMilli; import static java.util.Optional.ofNullable; @@ -38,85 +46,90 @@ import static org.apache.jena.system.Txn.calculateRead; @Log4j2 +@Service public class ViewService { private static final Query VALUES_QUERY = QueryFactory.create(String.format( """ - PREFIX fs: <%s> - PREFIX rdfs: + PREFIX fs: <%s> + PREFIX rdfs: - SELECT ?value ?label - WHERE { - ?value a ?type ; rdfs:label ?label . - FILTER EXISTS { - ?subject ?predicate ?value - FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } - } - FILTER NOT EXISTS { ?value fs:dateDeleted ?anyDateDeleted } - } ORDER BY ?label - """, + SELECT ?value ?label + WHERE { + ?value a ?type ; rdfs:label ?label . + FILTER EXISTS { + ?subject ?predicate ?value + FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } + } + FILTER NOT EXISTS { ?value fs:dateDeleted ?anyDateDeleted } + } ORDER BY ?label + """, FS.NS)); private static final Query RESOURCE_TYPE_VALUES_QUERY = QueryFactory.create(String.format( """ - PREFIX fs: <%s> - SELECT ?value ?label - WHERE { - VALUES (?value ?label) { - (fs:Collection "Collection") - (fs:Directory "Directory") - (fs:File "File") - } - } - """, + PREFIX fs: <%s> + SELECT ?value ?label + WHERE { + VALUES (?value ?label) { + (fs:Collection "Collection") + (fs:Directory "Directory") + (fs:File "File") + } + } + """, FS.NS)); private static final Query BOUNDS_QUERY = QueryFactory.create(String.format( """ - PREFIX fs: <%s> + PREFIX fs: <%s> - SELECT (MIN(?value) AS ?min) (MAX(?value) AS ?max) - WHERE { - ?subject ?predicate ?value - FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } - } - """, + SELECT (MIN(?value) AS ?min) (MAX(?value) AS ?max) + WHERE { + ?subject ?predicate ?value + FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } + } + """, FS.NS)); private static final Query BOOLEAN_VALUE_QUERY = QueryFactory.create(String.format( """ - PREFIX fs: <%s> - SELECT ?booleanValue - WHERE { - ?subject ?predicate ?booleanValue - FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } - } - """, + PREFIX fs: <%s> + SELECT ?booleanValue + WHERE { + ?subject ?predicate ?booleanValue + FILTER NOT EXISTS { ?subject fs:dateDeleted ?anyDateDeleted } + } + """, FS.NS)); public static final String USER_DOES_NOT_HAVE_PERMISSIONS_TO_READ_FACETS = "User does not have permissions to read facets"; - private final Config.Search searchConfig; - private final ViewsConfig viewsConfig; + private final SearchProperties searchProperties; + private final ViewsProperties viewsProperties; private final Dataset ds; + private final ViewStoreReader viewStoreReader; private final ViewStoreClientFactory viewStoreClientFactory; private final MetadataPermissions metadataPermissions; - private final LoadingCache> facetsCache; - private final LoadingCache> viewsCache; + private final LoadingCache> facetsCache; + private final LoadingCache> viewsCache; public ViewService( - Config config, - ViewsConfig viewsConfig, - Dataset ds, - ViewStoreClientFactory viewStoreClientFactory, + SearchProperties searchProperties, + CacheProperties cacheProperties, + ViewsProperties viewsProperties, + @Qualifier("filteredDataset") Dataset ds, + ViewStoreReader viewStoreReader, + @Nullable ViewStoreClientFactory viewStoreClientFactory, MetadataPermissions metadataPermissions) { - this.searchConfig = config.search; - this.viewsConfig = viewsConfig; + this.searchProperties = searchProperties; + this.viewsProperties = viewsProperties; this.ds = ds; + this.viewStoreReader = viewStoreReader; this.viewStoreClientFactory = viewStoreClientFactory; this.metadataPermissions = metadataPermissions; - this.facetsCache = buildCache(this::fetchFacets, config.caches.facets); - this.viewsCache = buildCache(this::fetchViews, config.caches.views); + this.facetsCache = buildCache(this::fetchFacets, cacheProperties.getFacets()); + this.viewsCache = buildCache(this::fetchViews, cacheProperties.getViews()); refreshCaches(); } @@ -132,7 +145,7 @@ public void refreshCaches() { log.info("Caches refreshing/warming up successfully finished"); } - public List getFacets() { + public List getFacets() { if (!metadataPermissions.canReadFacets()) { // this check is needed for cached data only as, otherwise, // the check will be performed during retrieving data from Jena @@ -145,7 +158,7 @@ public List getFacets() { } } - public List getViews() { + public List getViews() { try { return viewsCache.get(Boolean.TRUE); } catch (ExecutionException e) { @@ -153,57 +166,57 @@ public List getViews() { } } - protected List fetchViews() { - return viewsConfig.views.stream() + protected List fetchViews() { + return viewsProperties.views.stream() .map(v -> { - var columns = new ArrayList(); + var columns = new ArrayList(); // The entity label is the first column displayed, // if you want a column before this label, assign a negative displayIndex value in views.yaml final int ENTITY_LABEL_INDEX = 0; - columns.add(new ColumnDTO( + columns.add(new ColumnDto( v.name, v.itemName == null ? v.name : v.itemName, ColumnType.Identifier, ENTITY_LABEL_INDEX)); for (var c : v.columns) { - columns.add(new ColumnDTO(v.name + "_" + c.name, c.title, c.type, c.displayIndex)); + columns.add(new ColumnDto(v.name + "_" + c.name, c.title, c.type, c.displayIndex)); } for (var j : v.join) { - var joinView = viewsConfig.getViewConfig(j.view).orElse(null); + var joinView = viewsProperties.getViewConfig(j.view).orElse(null); if (joinView == null) { continue; } if (j.include.contains("id")) { - columns.add(new ColumnDTO( + columns.add(new ColumnDto( joinView.name, joinView.title, ColumnType.Identifier, j.displayIndex)); } for (var c : joinView.columns) { if (!j.include.contains(c.name)) { continue; } - columns.add(new ColumnDTO(joinView.name + "_" + c.name, c.title, c.type, j.displayIndex)); + columns.add(new ColumnDto(joinView.name + "_" + c.name, c.title, c.type, j.displayIndex)); } } - return new ViewDTO(v.name, v.title, columns, v.maxDisplayCount); + return new ViewDto(v.name, v.title, columns, v.maxDisplayCount); }) .collect(toList()); } - protected List fetchFacets() { - return calculateRead(ds, () -> viewsConfig.views.stream() + protected List fetchFacets() { + return calculateRead(ds, () -> viewsProperties.views.stream() .flatMap(view -> view.columns.stream() .map(column -> getFacetInfo(view, column)) - .filter(f -> (f.getMin() != null - || f.getMax() != null - || (f.getValues() != null && f.getValues().size() > 1) - || f.getBooleanValue() != null))) + .filter(f -> (f.min() != null + || f.max() != null + || (f.values() != null && f.values().size() > 1) + || f.booleanValue() != null))) .collect(toList())); } - private FacetDTO getFacetInfo(View view, View.Column column) { - List values = null; + private FacetDto getFacetInfo(View view, View.Column column) { + List values = null; Object min = null; Object max = null; Boolean booleanValue = null; @@ -223,7 +236,7 @@ private FacetDTO getFacetInfo(View view, View.Column column) { for (var row : (Iterable) execution::execSelect) { var resource = row.getResource("value"); var label = row.getLiteral("label").getString(); - values.add(new ValueDTO(label, resource.getURI())); + values.add(new ValueDto(label, resource.getURI())); } } } @@ -267,7 +280,7 @@ private FacetDTO getFacetInfo(View view, View.Column column) { } } - return new FacetDTO(view.name + "_" + column.name, column.title, column.type, values, booleanValue, min, max); + return new FacetDto(view.name + "_" + column.name, column.title, column.type, values, booleanValue, min, max); } private Object convertLiteralValue(Object value) { @@ -282,16 +295,14 @@ private Range getViewStoreColumnRange(View view, View.Column column) { if (!EnumSet.of(ColumnType.Date, ColumnType.Number).contains(column.type)) { return null; } - try (var reader = new ViewStoreReader(searchConfig, viewsConfig, viewStoreClientFactory)) { - return reader.aggregate(view.name, column.name); - } + return viewStoreReader.aggregate(view.name, column.name); } private LoadingCache> buildCache( - Supplier> fetchSupplier, Config.CacheConfig cacheConfig) { + Supplier> fetchSupplier, CacheProperties.Cache cacheConfig) { var cacheBuilder = CacheBuilder.newBuilder(); - if (cacheConfig.autoRefreshEnabled) { - cacheBuilder.refreshAfterWrite(cacheConfig.refreshFrequencyInHours, TimeUnit.HOURS); + if (cacheConfig.isAutoRefreshEnabled()) { + cacheBuilder.refreshAfterWrite(cacheConfig.getRefreshFrequencyInHours(), TimeUnit.HOURS); } return cacheBuilder.build(new CacheLoader<>() { @Override @@ -299,9 +310,9 @@ public List load(Boolean key) { var cachedObjects = fetchSupplier.get(); log.info( "List of {} has been cached, {} {} in total", - cacheConfig.name, + cacheConfig.getName(), cachedObjects.size(), - cacheConfig.name); + cacheConfig.getName()); return cachedObjects; } }); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java index 6545609449..112259331d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClient.java @@ -1,19 +1,31 @@ package io.fairspace.saturn.services.views; -import java.sql.*; -import java.time.*; -import java.util.*; -import java.util.function.*; -import java.util.stream.*; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.Getter; import lombok.SneakyThrows; -import lombok.extern.slf4j.*; -import org.apache.commons.lang3.tuple.*; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; -import io.fairspace.saturn.config.*; -import io.fairspace.saturn.config.ViewsConfig.*; -import io.fairspace.saturn.services.views.Table.*; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.services.views.Table.ColumnDefinition; import static io.fairspace.saturn.services.views.Table.idColumn; import static io.fairspace.saturn.services.views.Table.valueColumn; @@ -22,13 +34,14 @@ public class ViewStoreClient implements AutoCloseable { public static class ViewStoreConfiguration { - final Map viewConfig; + final Map viewConfig; final Map viewTables = new HashMap<>(); final Map> propertyTables = new HashMap<>(); final Map> joinTables = new HashMap<>(); - ViewStoreConfiguration(ViewsConfig viewsConfig) { - viewConfig = viewsConfig.views.stream().collect(Collectors.toMap(view -> view.name, Function.identity())); + public ViewStoreConfiguration(ViewsProperties viewsProperties) { + viewConfig = + viewsProperties.views.stream().collect(Collectors.toMap(view -> view.name, Function.identity())); } } @@ -134,7 +147,7 @@ int insertValues( public void updateValues(String view, String id, String property, Set values) throws SQLException { var propertyTable = configuration.propertyTables.get(view).get(property); - var valueColumn = valueColumn(property, ColumnType.Text); + var valueColumn = valueColumn(property, ViewsProperties.ColumnType.Text); var existing = retrieveValues(propertyTable.name, view, id, valueColumn); var deleteCount = deleteValues( diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java index 0a98e5c4eb..ace6e6332c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreClientFactory.java @@ -14,58 +14,46 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import lombok.Builder; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; -import io.fairspace.saturn.config.ViewsConfig.ColumnType; -import io.fairspace.saturn.config.ViewsConfig.View; +import io.fairspace.saturn.config.properties.ViewDatabaseProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties.ColumnType; +import io.fairspace.saturn.config.properties.ViewsProperties.View; import io.fairspace.saturn.vocabulary.FS; import static io.fairspace.saturn.services.views.Table.idColumn; import static io.fairspace.saturn.services.views.Table.valueColumn; @Slf4j +@Component +@ConditionalOnProperty(value = "application.view-database.enabled", havingValue = "true") public class ViewStoreClientFactory { + public static final Set protectedResources = Set.of(FS.COLLECTION_URI, FS.DIRECTORY_URI, FS.FILE_URI); + private final MaterializedViewService materializedViewService; - public ViewStoreClient build() throws SQLException { - return new ViewStoreClient(getConnection(), configuration, materializedViewService); - } + private final ViewStoreClient.ViewStoreConfiguration configuration; - public String databaseTypeForColumnType(ColumnType type) { - return switch (type) { - case Text, Term -> "text"; - case Date -> "timestamp"; - case Number -> "numeric"; - case Boolean -> "boolean"; - case Identifier -> "text not null"; - case Set, TermSet -> throw new IllegalArgumentException("No database type for column type set."); - }; - } - - public static final Set protectedResources = Set.of(FS.COLLECTION_URI, FS.DIRECTORY_URI, FS.FILE_URI); - - final ViewStoreClient.ViewStoreConfiguration configuration; public final DataSource dataSource; - public ViewStoreClientFactory(ViewsConfig viewsConfig, Config.ViewDatabase viewDatabase, Config.Search search) + public ViewStoreClientFactory( + ViewsProperties viewsProperties, + ViewDatabaseProperties viewDatabaseProperties, + MaterializedViewService materializedViewService, + DataSource dataSource, + ViewStoreClient.ViewStoreConfiguration configuration) throws SQLException { log.debug("Initializing the database connection"); - var databaseConfig = new HikariConfig(); - databaseConfig.setJdbcUrl(viewDatabase.url); - databaseConfig.setUsername(viewDatabase.username); - databaseConfig.setPassword(viewDatabase.password); - databaseConfig.setAutoCommit(viewDatabase.autoCommit); - databaseConfig.setConnectionTimeout(viewDatabase.connectionTimeout); - databaseConfig.setMaximumPoolSize(viewDatabase.maxPoolSize); - dataSource = new HikariDataSource(databaseConfig); + this.dataSource = dataSource; + this.materializedViewService = materializedViewService; + this.configuration = configuration; try (var connection = dataSource.getConnection()) { log.debug("Database connection: {}", connection.getMetaData().getDatabaseProductName()); @@ -75,23 +63,36 @@ public ViewStoreClientFactory(ViewsConfig viewsConfig, Config.ViewDatabase viewD "label", List.of(idColumn(), valueColumn("type", ColumnType.Text), valueColumn("label", ColumnType.Text)))); - configuration = new ViewStoreClient.ViewStoreConfiguration(viewsConfig); // todo: configuration is initialized within the loop below, do the initialization in constructor - for (View view : viewsConfig.views) { + for (View view : viewsProperties.views) { createOrUpdateView(view); } - materializedViewService = new MaterializedViewService(dataSource, configuration, search.maxJoinItems); - if (viewDatabase.mvRefreshOnStartRequired) { + if (viewDatabaseProperties.isMvRefreshOnStartRequired()) { materializedViewService.createOrUpdateAllMaterializedViews(); } else { log.warn("Skipping materialized view refresh on start"); } } + public ViewStoreClient build() throws SQLException { + return new ViewStoreClient(getConnection(), configuration, materializedViewService); + } + public Connection getConnection() throws SQLException { return dataSource.getConnection(); } + public String databaseTypeForColumnType(ColumnType type) { + return switch (type) { + case Text, Term -> "text"; + case Date -> "timestamp"; + case Number -> "numeric"; + case Boolean -> "boolean"; + case Identifier -> "text not null"; + case Set, TermSet -> throw new IllegalArgumentException("No database type for column type set."); + }; + } + Map getColumnMetadata(Connection connection, String table) throws SQLException { log.debug("Fetching metadata for {} ...", table); var resultSet = connection.getMetaData().getColumns(null, null, table, null); @@ -205,7 +206,7 @@ private void createIndexesIfNotExist(Table table, Connection connection) throws connection.setAutoCommit(false); } - void validateViewConfig(ViewsConfig.View view) { + void validateViewConfig(ViewsProperties.View view) { if (view.columns.stream().anyMatch(column -> "id".equalsIgnoreCase(column.name))) { throw new IllegalArgumentException("Forbidden to override the built-in column 'id' of view " + view.name); } @@ -223,7 +224,7 @@ void validateViewConfig(ViewsConfig.View view) { } } - void createOrUpdateView(ViewsConfig.View view) throws SQLException { + void createOrUpdateView(ViewsProperties.View view) throws SQLException { // Add view table validateViewConfig(view); var columns = new ArrayList(); @@ -244,7 +245,7 @@ void createOrUpdateView(ViewsConfig.View view) throws SQLException { // Add property tables var setColumns = view.columns.stream().filter(column -> column.type.isSet()).toList(); - for (ViewsConfig.View.Column column : setColumns) { + for (ViewsProperties.View.Column column : setColumns) { var propertyTableColumns = new ArrayList(); propertyTableColumns.add(idColumn(view.name)); propertyTableColumns.add(valueColumn(column.name, ColumnType.Identifier)); @@ -256,7 +257,7 @@ void createOrUpdateView(ViewsConfig.View view) throws SQLException { } if (view.join != null) { // Add join tables - for (ViewsConfig.View.JoinView join : view.join) { + for (ViewsProperties.View.JoinView join : view.join) { var joinTable = getJoinTable(join, view); createOrUpdateJoinTable(joinTable); configuration.joinTables.putIfAbsent(view.name, new HashMap<>()); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java index 3aaa8d43b5..e0dff7a398 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewStoreReader.java @@ -25,16 +25,18 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; - -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; -import io.fairspace.saturn.config.ViewsConfig.ColumnType; -import io.fairspace.saturn.config.ViewsConfig.View; -import io.fairspace.saturn.services.search.FileSearchRequest; -import io.fairspace.saturn.services.search.SearchResultDTO; +import org.springframework.stereotype.Component; + +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties.ColumnType; +import io.fairspace.saturn.config.properties.ViewsProperties.View; +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.vocabulary.FS; -import static io.fairspace.saturn.config.ViewsConfig.ColumnType.Date; +import static io.fairspace.saturn.config.properties.ViewsProperties.ColumnType.Date; import static io.fairspace.saturn.services.views.Table.idColumn; /** @@ -46,26 +48,28 @@ * collections in a filter with 'Resource_collection' as field. */ @Slf4j -public class ViewStoreReader implements AutoCloseable { - - final Config.Search searchConfig; - final ViewsConfig viewsConfig; +@Component +public class ViewStoreReader { + final SearchProperties searchProperties; + final ViewsProperties viewsProperties; final ViewStoreClient.ViewStoreConfiguration configuration; - final Connection connection; + final ViewStoreClientFactory viewStoreClientFactory; - // TODO: in whole class, use StringBuilder instead of String concats public ViewStoreReader( - Config.Search searchConfig, ViewsConfig viewsConfig, ViewStoreClientFactory viewStoreClientFactory) - throws SQLException { - this.searchConfig = searchConfig; - this.viewsConfig = viewsConfig; - this.configuration = viewStoreClientFactory.configuration; - this.connection = viewStoreClientFactory.getConnection(); + SearchProperties searchProperties, + ViewsProperties viewsProperties, + ViewStoreClientFactory viewStoreClientFactory, + ViewStoreClient.ViewStoreConfiguration configuration) { + this.searchProperties = searchProperties; + this.viewsProperties = viewsProperties; + this.configuration = configuration; + this.viewStoreClientFactory = viewStoreClientFactory; } List getLabelsByIds(List ids) throws SQLException { String query = "select label from label where id = ANY(?::text[])"; - try (var preparedStatement = connection.prepareStatement(query)) { + try (var connection = viewStoreClientFactory.getConnection(); + var preparedStatement = connection.prepareStatement(query)) { var array = preparedStatement.getConnection().createArrayOf("text", ids.toArray()); preparedStatement.setArray(1, array); var result = preparedStatement.executeQuery(); @@ -78,7 +82,8 @@ List getLabelsByIds(List ids) throws SQLException { } String iriForLabel(String type, String label) throws SQLException { - try (var query = connection.prepareStatement("select id from label where type = ? and label = ?")) { + try (var connection = viewStoreClientFactory.getConnection(); + var query = connection.prepareStatement("select id from label where type = ? and label = ?")) { query.setString(1, type); query.setString(2, label); var result = query.executeQuery(); @@ -89,14 +94,14 @@ String iriForLabel(String type, String label) throws SQLException { return null; } - Map> transformRow(View viewConfig, ResultSet result) throws SQLException { - Map> row = new HashMap<>(); + Map> transformRow(View viewConfig, ResultSet result) throws SQLException { + Map> row = new HashMap<>(); row.put( viewConfig.name, - Collections.singleton(new ValueDTO(result.getString("label"), result.getString("id")))); + Collections.singleton(new ValueDto(result.getString("label"), result.getString("id")))); if (viewConfig.name.equalsIgnoreCase("Collection")) { var collection = result.getString("collection"); - row.put(viewConfig.name + "_collection", Collections.singleton(new ValueDTO(collection, collection))); + row.put(viewConfig.name + "_collection", Collections.singleton(new ValueDto(collection, collection))); } for (var viewColumn : viewConfig.columns) { if (viewColumn.type.isSet()) { @@ -107,23 +112,23 @@ Map> transformRow(View viewConfig, ResultSet result) throw if (column.type == ColumnType.Number) { var value = result.getBigDecimal(column.name); if (value != null) { - row.put(columnName, Collections.singleton(new ValueDTO(value.toString(), value.floatValue()))); + row.put(columnName, Collections.singleton(new ValueDto(value.toString(), value.floatValue()))); } } else if (column.type == Date) { var value = result.getTimestamp(column.name); if (value != null) { row.put( columnName, - Collections.singleton(new ValueDTO(value.toInstant().toString(), value.toInstant()))); + Collections.singleton(new ValueDto(value.toInstant().toString(), value.toInstant()))); } } else { var value = result.getString(column.name); if (viewColumn.type == ColumnType.Term) { row.put( columnName, - Collections.singleton(new ValueDTO(value, iriForLabel(viewColumn.rdfType, value)))); + Collections.singleton(new ValueDto(value, iriForLabel(viewColumn.rdfType, value)))); } else { - row.put(columnName, Collections.singleton(new ValueDTO(value, value))); + row.put(columnName, Collections.singleton(new ValueDto(value, value))); } } } @@ -292,7 +297,8 @@ String sqlFilter(String alias, View view, List filters, List .collect(Collectors.joining(" and ")); } - PreparedStatement query(String view, List filters, String scope, boolean isCount) throws SQLException { + PreparedStatement query(Connection connection, String view, List filters, String scope, boolean isCount) + throws SQLException { if (filters == null) { filters = Collections.emptyList(); } @@ -351,7 +357,7 @@ PreparedStatement query(String view, List filters, String scope, boo } private String transformToCountQuery(String viewName, String query) { - boolean isCountLimitDefined = viewsConfig + boolean isCountLimitDefined = viewsProperties .getViewConfig(viewName) .map(c -> c.maxDisplayCount != null) .orElse(false); @@ -361,7 +367,7 @@ private String transformToCountQuery(String viewName, String query) { // we just set limit for the query and then wrap with count query // on UI user either exact number if less the limit or "more than 'limit'" if more query = "select count(*) as rowCount from ( " + query + " limit %s) as count"; - query = query.formatted(viewsConfig + query = query.formatted(viewsProperties .getViewConfig(viewName) .map(c -> c.maxDisplayCount) .orElseThrow()); @@ -402,12 +408,15 @@ Map retrieveViewTableRows(String view, List filters private Map getViewRowsForNonSetType(View view, List filters, int offset, int limit) throws SQLException { - try (var query = query( - view.name, - filters, - String.format("order by id %s limit %d", offset > 0 ? String.format("offset %d", offset) : "", limit), - false)) { - query.setQueryTimeout((int) searchConfig.pageRequestTimeout); + try (var connection = viewStoreClientFactory.getConnection(); + var query = query( + connection, + view.name, + filters, + String.format( + "order by id %s limit %d", offset > 0 ? String.format("offset %d", offset) : "", limit), + false)) { + query.setQueryTimeout(searchProperties.getPageRequestTimeout()); var result = query.executeQuery(); Map rowsById = new HashMap<>(); while (result.next()) { @@ -424,7 +433,8 @@ private Map getViewRowsForSetType(String view, List val var columns = String.join(", ", valueSetProperties); var query = "select %sid, %s from mv_%s where %sid = ANY(?::text[])".formatted(view, columns, view, view); - try (PreparedStatement ps = connection.prepareStatement(query)) { + try (var connection = viewStoreClientFactory.getConnection(); + var ps = connection.prepareStatement(query)) { Array array = ps.getConnection().createArrayOf("text", viewIds); ps.setArray(1, array); ResultSet resultSet = ps.executeQuery(); @@ -452,10 +462,11 @@ private ViewRowCollection retrieveJoinTableRows(String view, View.JoinView joinV joinView.include.stream().map(i -> joinView.view + "_" + i)) .collect(Collectors.toSet()); - var rows = new ViewRowCollection(searchConfig.maxJoinItems); + var rows = new ViewRowCollection(searchProperties.getMaxJoinItems()); if (!ids.isEmpty()) { - try (var query = getJoinQuery(view, joinedTable, ids); + try (var connection = viewStoreClientFactory.getConnection(); + var query = getJoinQuery(connection, view, joinedTable, ids); var result = query.executeQuery()) { while (result.next()) { var id = result.getString(viewIdColumn); @@ -477,7 +488,7 @@ private ViewRow buildJoinRows( if (joinViewId != null) { // could be null as we do the left join for join views row.put( joinView.view, - Sets.newHashSet(new ValueDTO( + Sets.newHashSet(new ValueDto( result.getString(joinView.view + "_label"), result.getString(joinViewIdName)))); for (var column : projectionColumns) { var columnDefinition = Optional.ofNullable( @@ -496,28 +507,27 @@ private static void parseAndSetValueForColumn( if (columnDefinition.type == ColumnType.Number) { var value = result.getBigDecimal(columnDefinition.name); if (value != null) { - row.put(columnDefinition.name, Sets.newHashSet(new ValueDTO(value.toString(), value))); + row.put(columnDefinition.name, Sets.newHashSet(new ValueDto(value.toString(), value))); } } else if (columnDefinition.type == Date) { var value = result.getTimestamp(columnDefinition.name); if (value != null) { row.put( columnDefinition.name, - Sets.newHashSet(new ValueDTO(value.toInstant().toString(), value.toString()))); + Sets.newHashSet(new ValueDto(value.toInstant().toString(), value.toString()))); } } else { var label = result.getString(columnDefinition.name); if (label != null) { - row.put(columnDefinition.name, Sets.newHashSet(new ValueDTO(label, label))); + row.put(columnDefinition.name, Sets.newHashSet(new ValueDto(label, label))); } } } - private PreparedStatement getJoinQuery(String view, Table joinedTable, Collection ids) throws SQLException { - + private PreparedStatement getJoinQuery( + Connection connection, String view, Table joinedTable, Collection ids) throws SQLException { var query = "select * from mv_%s_join_%s where %s = ANY(?::text[])" .formatted(view, joinedTable.name, idColumn(view).name); - var ps = connection.prepareStatement(query); var array = ps.getConnection().createArrayOf("text", ids.toArray()); ps.setArray(1, array); @@ -538,8 +548,9 @@ public Range aggregate(String view, String column) { } var table = configuration.viewTables.get(view); var columnDefinition = table.getColumn(column.toLowerCase()); - try (PreparedStatement query = connection.prepareStatement("select min(" + columnDefinition.name - + ") as min, max(" + columnDefinition.name + ") as max" + " from " + table.name)) { + try (var connection = viewStoreClientFactory.getConnection(); + var query = connection.prepareStatement("select min(" + columnDefinition.name + ") as min, max(" + + columnDefinition.name + ") as max" + " from " + table.name)) { var result = query.executeQuery(); if (!result.next()) { return null; @@ -571,7 +582,7 @@ public Range aggregate(String view, String column) { * @param includeJoinedViews if true, include joined views in the resulting rows. * @return the list of rows. */ - public List>> retrieveRows( + public List>> retrieveRows( String view, List filters, int offset, int limit, boolean includeJoinedViews) { try { var viewConfig = configuration.viewConfig.get(view); @@ -599,8 +610,9 @@ private void addJoinTableRowsForRow(ViewRow viewRow, List allJoinTableR } public long countRows(String view, List filters) throws SQLTimeoutException { - try (var q = query(view, filters, null, true)) { - q.setQueryTimeout((int) searchConfig.countRequestTimeout); + try (var connection = viewStoreClientFactory.getConnection(); + var q = query(connection, view, filters, null, true)) { + q.setQueryTimeout(searchProperties.getCountRequestTimeout()); var result = q.executeQuery(); result.next(); return result.getLong("rowCount"); @@ -611,7 +623,7 @@ public long countRows(String view, List filters) throws SQLTimeoutEx } } - public List searchFiles(FileSearchRequest request, List userCollections) { + public List searchFiles(FileSearchRequest request, List userCollections) { if (userCollections == null || userCollections.isEmpty()) { return Collections.emptyList(); } @@ -637,12 +649,13 @@ public List searchFiles(FileSearchRequest request, List .append(idConstraint) .append("order by id asc limit 1000"); - try (var statement = connection.prepareStatement(queryString.toString())) { + try (var connection = viewStoreClientFactory.getConnection(); + var statement = connection.prepareStatement(queryString.toString())) { for (int i = 0; i < values.size(); i++) { statement.setString(i + 1, values.get(i)); } - statement.setQueryTimeout((int) searchConfig.pageRequestTimeout); + statement.setQueryTimeout(searchProperties.getPageRequestTimeout()); var result = statement.executeQuery(); return convertResult(result); @@ -654,10 +667,10 @@ public List searchFiles(FileSearchRequest request, List } @SneakyThrows - private List convertResult(ResultSet resultSet) { - var rows = new ArrayList(); + private List convertResult(ResultSet resultSet) { + var rows = new ArrayList(); while (resultSet.next()) { - var row = SearchResultDTO.builder() + var row = SearchResultDto.builder() .id(resultSet.getString("id")) .label(resultSet.getString("label")) .type(FS.NS + resultSet.getString("type")) @@ -668,9 +681,4 @@ private List convertResult(ResultSet resultSet) { } return rows; } - - @Override - public void close() throws Exception { - connection.close(); - } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java index d665682d37..c9bfef0950 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewUpdater.java @@ -28,26 +28,30 @@ import org.apache.jena.vocabulary.RDFS; import org.apache.jena.vocabulary.XSD; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.vocabulary.FS; -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; -import static io.fairspace.saturn.config.ConfigLoader.VIEWS_CONFIG; import static io.fairspace.saturn.services.views.Table.idColumn; import static io.fairspace.saturn.services.views.Table.valueColumn; import static io.fairspace.saturn.services.views.ViewStoreClientFactory.protectedResources; @Slf4j public class ViewUpdater implements AutoCloseable { + + private final ViewsProperties viewsProperties; private final ViewStoreClient viewStoreClient; private final DatasetGraph dsg; private final Graph graph; + private final String publicUrl; - public ViewUpdater(ViewStoreClient viewStoreClient, DatasetGraph dsg) { + public ViewUpdater( + ViewsProperties viewsProperties, ViewStoreClient viewStoreClient, DatasetGraph dsg, String publicUrl) { + this.viewsProperties = viewsProperties; this.viewStoreClient = viewStoreClient; this.dsg = dsg; this.graph = dsg.getDefaultGraph(); + this.publicUrl = publicUrl; } @Override @@ -86,10 +90,10 @@ private static String getLabel(Graph graph, Node subject) { if (labelNode == null) { return null; } - return labelNode.toString(false); + return labelNode.getLiteral().toString(false); } - public Object getValue(ViewsConfig.View.Column column, Node node) throws SQLException { + public Object getValue(ViewsProperties.View.Column column, Node node) throws SQLException { return switch (column.type) { case Boolean, Number -> node.getLiteralValue(); case Date -> { @@ -127,7 +131,7 @@ public Object getValue(ViewsConfig.View.Column column, Node node) throws SQLExce private void addCollectionToProtectedResourceRow(String type, Node subject, Map row) { if (protectedResources.contains(type)) { // set collection name - var rootLocation = CONFIG.publicUrl + "/api/webdav" + "/"; + var rootLocation = publicUrl + "/api/webdav" + "/"; if (subject.getURI().startsWith(rootLocation)) { var location = subject.getURI().substring(rootLocation.length()); var collection = URLDecoder.decode(location.split("/")[0], StandardCharsets.UTF_8); @@ -148,7 +152,7 @@ public void updateSubject(Node subject) { var start = new Date().getTime(); var type = typeNode.get().getObject(); log.debug("Subject {} of type {}", subject.getURI(), type.getLocalName()); - VIEWS_CONFIG.views.stream() + viewsProperties.views.stream() .filter(view -> view.types.contains(type.getURI())) .forEach(view -> { if (graph.find(subject, FS.dateDeleted.asNode(), Node.ANY).hasNext()) { @@ -183,7 +187,7 @@ public void updateSubject(Node subject) { log.error("Failed to update view row", e); } // Update subject value sets - for (ViewsConfig.View.Column column : view.columns) { + for (ViewsProperties.View.Column column : view.columns) { if (!column.type.isSet()) { continue; } @@ -194,7 +198,7 @@ public void updateSubject(Node subject) { try { var values = new HashSet(); for (var term : objects) { - if (column.type == ViewsConfig.ColumnType.TermSet) { + if (column.type == ViewsProperties.ColumnType.TermSet) { var label = getLabel(graph, term); viewStoreClient.addLabel(term.getURI(), column.rdfType, label); values.add(label); @@ -240,7 +244,7 @@ public void updateSubject(Node subject) { /** * Only use this method in a secure and synchonisized way, see 'MaintenanceService.recreateIndex()' */ - public void recreateIndexForView(ViewStoreClient viewStoreClient, ViewsConfig.View view) throws SQLException { + public void recreateIndexForView(ViewStoreClient viewStoreClient, ViewsProperties.View view) throws SQLException { // Clear database tables for view log.info("Recreating index for view {} started", view.name); viewStoreClient.truncateViewTables(view.name); @@ -261,7 +265,7 @@ public void recreateIndexForView(ViewStoreClient viewStoreClient, ViewsConfig.Vi } private Map transformResult( - String type, List columns, QuerySolution result) throws SQLException { + String type, List columns, QuerySolution result) throws SQLException { var values = new HashMap(); var subject = result.getResource("id"); values.put("id", subject.getURI()); @@ -285,7 +289,7 @@ private Map transformResult( * @param view The view for which to update the values. * @param type The subject type (for when the view includes multiple types) */ - public void copyValuesForType(ViewsConfig.View view, String type) throws SQLException { + public void copyValuesForType(ViewsProperties.View view, String type) throws SQLException { var columns = view.columns.stream().filter(column -> !column.type.isSet()).collect(Collectors.toList()); var attributes = columns.stream() @@ -340,13 +344,13 @@ public void copyValuesForType(ViewsConfig.View view, String type) throws SQLExce * @param type The subject type (for when the view includes multiple types) * @param column The view column of value set property. */ - public void copyValueSetsForColumn(ViewsConfig.View view, String type, ViewsConfig.View.Column column) + public void copyValueSetsForColumn(ViewsProperties.View view, String type, ViewsProperties.View.Column column) throws SQLException { var property = column.name; var propertyTable = viewStoreClient.getConfiguration().propertyTables.get(view.name).get(property); var idColumn = idColumn(view.name); - var propertyColumn = valueColumn(column.name, ViewsConfig.ColumnType.Identifier); + var propertyColumn = valueColumn(column.name, ViewsProperties.ColumnType.Identifier); var predicate = Arrays.stream(column.source.split("\\s+")) .map("<%s>"::formatted) .collect(Collectors.joining("/")); @@ -400,7 +404,8 @@ public void copyValueSetsForColumn(ViewsConfig.View view, String type, ViewsConf * @param type The subject type (for when the view includes multiple types) * @param join The join relation. */ - public void copyLinks(ViewsConfig.View view, String type, ViewsConfig.View.JoinView join) throws SQLException { + public void copyLinks(ViewsProperties.View view, String type, ViewsProperties.View.JoinView join) + throws SQLException { var joinTable = viewStoreClient.getConfiguration().joinTables.get(view.name).get(join.view); var idColumn = idColumn(view.name); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java deleted file mode 100644 index ccecb3e7a7..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/views/ViewsDTO.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.fairspace.saturn.services.views; - -import java.util.List; - -import lombok.Value; - -@Value -public class ViewsDTO { - List views; -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java deleted file mode 100644 index 6b259fabab..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceApp.java +++ /dev/null @@ -1,49 +0,0 @@ -package io.fairspace.saturn.services.workspaces; - -import io.fairspace.saturn.services.BaseApp; - -import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; -import static org.apache.jena.graph.NodeFactory.createURI; -import static org.eclipse.jetty.http.MimeTypes.Type.APPLICATION_JSON; -import static spark.Spark.*; - -public class WorkspaceApp extends BaseApp { - private final WorkspaceService workspaceService; - - public WorkspaceApp(String basePath, WorkspaceService workspaceService) { - super(basePath); - this.workspaceService = workspaceService; - } - - @Override - protected void initApp() { - put("/", (req, res) -> { - var ws = workspaceService.createWorkspace(mapper.readValue(req.body(), Workspace.class)); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(ws); - }); - - get("/", (req, res) -> { - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(workspaceService.listWorkspaces()); - }); - - delete("/", (req, res) -> { - workspaceService.deleteWorkspace(createURI(req.queryParams("workspace"))); - res.status(SC_NO_CONTENT); - return ""; - }); - - get("/users/", (req, res) -> { - var users = workspaceService.getUsers(createURI(req.queryParams("workspace"))); - res.type(APPLICATION_JSON.asString()); - return mapper.writeValueAsString(users); - }); - - patch("/users/", (req, res) -> { - var dto = mapper.readValue(req.body(), UserRoleDto.class); - workspaceService.setUserRole(dto.getWorkspace(), dto.getUser(), dto.getRole()); - return ""; - }); - } -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java index aca6bf19a0..3b906fa91f 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/services/workspaces/WorkspaceService.java @@ -5,12 +5,14 @@ import java.util.Map; import java.util.Optional; -import lombok.extern.log4j.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; import org.apache.jena.graph.Node; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import org.springframework.stereotype.Service; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -27,14 +29,13 @@ import static java.util.stream.Collectors.toList; @Log4j2 +@Service +@RequiredArgsConstructor public class WorkspaceService { + private final Transactions tx; - private final UserService userService; - public WorkspaceService(Transactions tx, UserService userService) { - this.tx = tx; - this.userService = userService; - } + private final UserService userService; public List listWorkspaces() { return tx.calculateRead(m -> { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/vocabulary/Vocabularies.java b/projects/saturn/src/main/java/io/fairspace/saturn/vocabulary/Vocabularies.java deleted file mode 100644 index 28ee69e5b5..0000000000 --- a/projects/saturn/src/main/java/io/fairspace/saturn/vocabulary/Vocabularies.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.fairspace.saturn.vocabulary; - -import org.apache.jena.rdf.model.Model; - -import static org.apache.jena.riot.RDFDataMgr.loadModel; - -public class Vocabularies { - public static final Model SYSTEM_VOCABULARY = loadModel("system-vocabulary.ttl"); - public static final Model USER_VOCABULARY = loadModel("vocabulary.ttl"); - public static final Model VOCABULARY = SYSTEM_VOCABULARY.union(USER_VOCABULARY); -} diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/DavFactory.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/DavFactory.java index 2a5c73c688..68dc3a0ddf 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/DavFactory.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/DavFactory.java @@ -6,13 +6,20 @@ import io.milton.http.exceptions.NotAuthorizedException; import io.milton.resource.Resource; import org.apache.jena.graph.Node; +import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.util.Context; import org.apache.jena.vocabulary.RDF; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.vocabulary.FS; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import io.fairspace.saturn.webdav.resources.*; +import io.fairspace.saturn.webdav.resources.CollectionResource; +import io.fairspace.saturn.webdav.resources.CollectionRootResource; +import io.fairspace.saturn.webdav.resources.DirectoryResource; +import io.fairspace.saturn.webdav.resources.ExtraStorageRootResource; +import io.fairspace.saturn.webdav.resources.FileResource; +import io.fairspace.saturn.webdav.resources.RootResource; import static io.fairspace.saturn.auth.RequestContext.getUserURI; import static io.fairspace.saturn.util.EnumUtils.max; @@ -29,22 +36,32 @@ public class DavFactory implements ResourceFactory { public final UserService userService; public final Context context; public final RootResource root; + public final Model userVocabulary; + public final Model vocabulary; // Represents the root URI, not stored in the database private final String baseUri; public DavFactory( - org.apache.jena.rdf.model.Resource rootSubject, BlobStore store, UserService userService, Context context) { + org.apache.jena.rdf.model.Resource rootSubject, + BlobStore store, + UserService userService, + Context context, + WebDavProperties webDavProperties, + Model userVocabulary, + Model vocabulary) { this.rootSubject = rootSubject; this.store = store; this.userService = userService; this.context = context; + this.userVocabulary = userVocabulary; + this.vocabulary = vocabulary; var uri = URI.create(rootSubject.getURI()); this.baseUri = URI.create( uri.getScheme() + "://" + uri.getHost() + (uri.getPort() > 0 ? ":" + uri.getPort() : "")) .toString(); root = uri.toString().endsWith("/api/webdav") ? new CollectionRootResource(this) - : new ExtraStorageRootResource(this); + : new ExtraStorageRootResource(this, webDavProperties); } @Override @@ -81,16 +98,16 @@ public Resource getResource(org.apache.jena.rdf.model.Resource subject, Access a public Resource getResourceByType(org.apache.jena.rdf.model.Resource subject, Access access) { if (subject.hasProperty(RDF.type, FS.File)) { - return new FileResource(this, subject, access); + return new FileResource(this, subject, access, userVocabulary); } if (subject.hasProperty(RDF.type, FS.Directory)) { - return new DirectoryResource(this, subject, access); + return new DirectoryResource(this, subject, access, userVocabulary, vocabulary); } if (subject.hasProperty(RDF.type, FS.Collection)) { - return new CollectionResource(this, subject, access); + return new CollectionResource(this, subject, access, userVocabulary, vocabulary); } if (subject.hasProperty(RDF.type, FS.ExtraStorageDirectory)) { - return new DirectoryResource(this, subject, access); + return new DirectoryResource(this, subject, access, userVocabulary, vocabulary); } return null; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/PreParsedServletRequest.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/PreParsedServletRequest.java index cb77dfa61f..07ea12eb0d 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/PreParsedServletRequest.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/PreParsedServletRequest.java @@ -2,11 +2,11 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.http.HttpServletRequest; import io.milton.http.FileItem; import io.milton.http.RequestParseException; import io.milton.servlet.ServletRequest; +import jakarta.servlet.http.HttpServletRequest; import io.fairspace.saturn.webdav.blobstore.BlobFileItem; import io.fairspace.saturn.webdav.blobstore.BlobStore; diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/TransactionalHandlerWrapper.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/TransactionalHandlerWrapper.java index 4f7aeaec7c..87ddaf9b0e 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/TransactionalHandlerWrapper.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/TransactionalHandlerWrapper.java @@ -4,10 +4,7 @@ import io.milton.http.HttpManager; import io.milton.http.Request; import io.milton.http.Response; -import io.milton.http.exceptions.BadRequestException; -import io.milton.http.exceptions.ConflictException; -import io.milton.http.exceptions.NotAuthorizedException; -import io.milton.http.exceptions.NotFoundException; +import io.milton.http.exceptions.MiltonException; import io.milton.resource.Resource; import lombok.SneakyThrows; @@ -29,12 +26,19 @@ public String[] getMethods() { @Override @SneakyThrows - public void process(HttpManager httpManager, Request request, Response response) - throws ConflictException, NotAuthorizedException, BadRequestException, NotFoundException { + public void process(HttpManager httpManager, Request request, Response response) { if (request.getMethod().isWrite) { - txn.executeWrite(ds -> wrapped.process(httpManager, request, response)); + try { + txn.executeWrite(ds -> wrapped.process(httpManager, request, response)); + } catch (MiltonException e) { + throw new RuntimeException(e); + } } else { - txn.executeRead(ds -> wrapped.process(httpManager, request, response)); + try { + txn.executeRead(ds -> wrapped.process(httpManager, request, response)); + } catch (MiltonException e) { + throw new RuntimeException(e); + } } } diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVServlet.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVServlet.java index f078b044bb..3a186285dd 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVServlet.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/WebDAVServlet.java @@ -3,25 +3,30 @@ import java.io.IOException; import java.time.Instant; import java.util.Optional; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import io.milton.config.HttpManagerBuilder; import io.milton.event.ResponseEvent; -import io.milton.http.*; +import io.milton.http.AuthenticationService; +import io.milton.http.HttpManager; +import io.milton.http.ProtocolHandlers; +import io.milton.http.Request; +import io.milton.http.RequestParseException; +import io.milton.http.ResourceFactory; +import io.milton.http.Response; import io.milton.http.http11.DefaultHttp11ResponseHandler; import io.milton.http.webdav.ResourceTypeHelper; import io.milton.http.webdav.WebDavResponseHandler; import io.milton.resource.Resource; import io.milton.servlet.ServletResponse; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.apache.jena.rdf.model.Literal; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.App.API_PREFIX; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static io.fairspace.saturn.rdf.SparqlUtils.toXSDDateTimeLiteral; @@ -42,6 +47,7 @@ public class WebDAVServlet extends HttpServlet { private static final String TIMESTAMP_ATTRIBUTE = "TIMESTAMP"; public static final String POST_COMMIT_ACTION_ATTRIBUTE = "POST_COMMIT"; public static final String ERROR_MESSAGE = "ERROR_MESSAGE"; + public static final String VERSION = "version"; private final HttpManager httpManager; private final BlobStore store; @@ -123,7 +129,9 @@ protected void service(HttpServletRequest req, HttpServletResponse res) throws I public static Integer fileVersion() { return Optional.ofNullable(getCurrentRequest()) - .map(r -> (isEmpty(r.getParameter("version")) ? r.getHeader("Version") : r.getParameter("version"))) + .map(r -> (isEmpty(getCurrentRequest().getParameter(VERSION)) + ? r.getHeader("Version") + : getCurrentRequest().getParameter(VERSION))) .map(Integer::parseInt) .orElse(null); } @@ -143,7 +151,8 @@ public static boolean includeMetadataLinks() { } public static boolean isMetadataRequest() { - return (API_PREFIX + "/metadata/").equalsIgnoreCase(getCurrentRequest().getServletPath()); + // todo: test the change + return ("/api/metadata/").equalsIgnoreCase(getCurrentRequest().getRequestURI()); } public static BlobInfo getBlob() { diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/BaseResource.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/BaseResource.java index 055dc13be7..2c774d3fb6 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/BaseResource.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/BaseResource.java @@ -17,6 +17,7 @@ import io.milton.resource.*; import org.apache.jena.rdf.model.*; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; import org.apache.jena.shacl.vocabulary.SHACL; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; @@ -30,7 +31,6 @@ import static io.fairspace.saturn.auth.RequestContext.getUserURI; import static io.fairspace.saturn.rdf.ModelUtils.*; import static io.fairspace.saturn.rdf.SparqlUtils.parseXSDDateTimeLiteral; -import static io.fairspace.saturn.vocabulary.Vocabularies.USER_VOCABULARY; import static io.fairspace.saturn.webdav.DavFactory.childSubject; import static io.fairspace.saturn.webdav.WebDAVServlet.*; @@ -52,11 +52,13 @@ public abstract class BaseResource protected final DavFactory factory; public final Resource subject; protected final Access access; + private final Model userVocabulary; - BaseResource(DavFactory factory, Resource subject, Access access) { + BaseResource(DavFactory factory, Resource subject, Access access, Model userVocabulary) { this.factory = factory; this.subject = subject; this.access = access; + this.userVocabulary = userVocabulary; } @Override @@ -326,7 +328,7 @@ public String getMetadataLinks() { } public Set metadataLinks() { - var userVocabularyPaths = USER_VOCABULARY + var userVocabularyPaths = userVocabulary .listStatements() .filterKeep(stmt -> stmt.getObject().isResource() && stmt.getPredicate().getURI().equals(SHACL.path.getURI())) diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/CollectionResource.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/CollectionResource.java index 48fb9fe4c6..1c511898dc 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/CollectionResource.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/CollectionResource.java @@ -11,6 +11,7 @@ import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.ConflictException; import io.milton.http.exceptions.NotAuthorizedException; +import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.Statement; import org.apache.jena.vocabulary.RDF; @@ -27,8 +28,9 @@ public class CollectionResource extends DirectoryResource { - public CollectionResource(DavFactory factory, Resource subject, Access access) { - super(factory, subject, access); + public CollectionResource( + DavFactory factory, Resource subject, Access access, Model userVocabulary, Model vocabulary) { + super(factory, subject, access, userVocabulary, vocabulary); } @Override diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/DirectoryResource.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/DirectoryResource.java index 9adb0b2d26..941ed5fbe6 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/DirectoryResource.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/DirectoryResource.java @@ -6,7 +6,12 @@ import java.io.OutputStream; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.*; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import io.milton.http.Auth; @@ -21,6 +26,7 @@ import io.milton.resource.FolderResource; import io.milton.resource.Resource; import org.apache.commons.csv.CSVFormat; +import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.RDFNode; import org.apache.jena.rdf.model.Statement; import org.apache.jena.shacl.vocabulary.SHACLM; @@ -36,11 +42,12 @@ import io.fairspace.saturn.webdav.blobstore.BlobFileItem; import io.fairspace.saturn.webdav.blobstore.BlobInfo; -import static io.fairspace.saturn.config.Services.METADATA_SERVICE; +import static io.fairspace.saturn.config.MetadataConfig.METADATA_SERVICE; import static io.fairspace.saturn.rdf.ModelUtils.getStringProperty; -import static io.fairspace.saturn.vocabulary.Vocabularies.VOCABULARY; import static io.fairspace.saturn.webdav.DavFactory.childSubject; -import static io.fairspace.saturn.webdav.PathUtils.*; +import static io.fairspace.saturn.webdav.PathUtils.encodePath; +import static io.fairspace.saturn.webdav.PathUtils.normalizePath; +import static io.fairspace.saturn.webdav.PathUtils.splitPath; import static io.fairspace.saturn.webdav.WebDAVServlet.getBlob; import static io.fairspace.saturn.webdav.WebDAVServlet.setErrorMessage; @@ -50,8 +57,17 @@ import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; public class DirectoryResource extends BaseResource implements FolderResource, DeletableCollectionResource { - public DirectoryResource(DavFactory factory, org.apache.jena.rdf.model.Resource subject, Access access) { - super(factory, subject, access); + + private final Model vocabulary; + + public DirectoryResource( + DavFactory factory, + org.apache.jena.rdf.model.Resource subject, + Access access, + Model userVocabulary, + Model vocabulary) { + super(factory, subject, access, userVocabulary); + this.vocabulary = vocabulary; } @Override @@ -276,7 +292,7 @@ private void uploadMetadata(FileItem file) throws BadRequestException, ConflictE throw new BadRequestException(this); } - var classShape = s.getPropertyResourceValue(RDF.type).inModel(VOCABULARY); + var classShape = s.getPropertyResourceValue(RDF.type).inModel(vocabulary); var propertyShapes = new HashMap(); classShape @@ -358,6 +374,7 @@ private void uploadMetadata(FileItem file) throws BadRequestException, ConflictE throw new BadRequestException("Error parsing file " + file.getName(), e); } + // TODO: This is an old solution, refactor: metadataService is to be a field passed via constructor MetadataService metadataService = factory.context.get(METADATA_SERVICE); try { metadataService.patch(model, Boolean.TRUE); @@ -367,7 +384,7 @@ private void uploadMetadata(FileItem file) throws BadRequestException, ConflictE for (var v : e.getViolations()) { var path = v.getSubject().replaceFirst(subject.getURI(), ""); path = URLDecoder.decode(path, StandardCharsets.UTF_8); - var propertyShapes = VOCABULARY.listResourcesWithProperty(SHACLM.path, createURI(v.getPredicate())); + var propertyShapes = vocabulary.listResourcesWithProperty(SHACLM.path, createURI(v.getPredicate())); var propertyName = propertyShapes.hasNext() ? getStringProperty(propertyShapes.next(), SHACLM.name) : createURI(v.getPredicate()).getLocalName(); diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/ExtraStorageRootResource.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/ExtraStorageRootResource.java index 01710339a6..8856ef118c 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/ExtraStorageRootResource.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/ExtraStorageRootResource.java @@ -4,8 +4,6 @@ import java.util.Objects; import java.util.Optional; -import io.milton.http.Auth; -import io.milton.http.Request; import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.ConflictException; import io.milton.http.exceptions.NotAuthorizedException; @@ -15,11 +13,11 @@ import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.vocabulary.FS; import io.fairspace.saturn.webdav.Access; import io.fairspace.saturn.webdav.DavFactory; -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; import static io.fairspace.saturn.webdav.DavFactory.childSubject; import static io.milton.http.ResponseStatus.SC_FORBIDDEN; @@ -27,13 +25,11 @@ @Log4j2 public class ExtraStorageRootResource extends RootResource { - public ExtraStorageRootResource(DavFactory factory) { - super(factory); - } + private final WebDavProperties webDavProperties; - @Override - public boolean authorise(Request request, Request.Method method, Auth auth) { - return true; + public ExtraStorageRootResource(DavFactory factory, WebDavProperties webDavProperties) { + super(factory); + this.webDavProperties = webDavProperties; } @Override @@ -49,7 +45,7 @@ public List getChildren() { @Override public CollectionResource createCollection(String name) throws ConflictException, BadRequestException, NotAuthorizedException { - if (!CONFIG.extraStorage.defaultRootCollections.contains(name)) { + if (!webDavProperties.getExtraStorage().getDefaultRootCollections().contains(name)) { // Currently all root extra storage directories should be specified in the extra storage config throw new NotAuthorizedException( String.format("Directory with name %s not specified in the configuration.", name), diff --git a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/FileResource.java b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/FileResource.java index 1d39daae53..4a0eca7461 100644 --- a/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/FileResource.java +++ b/projects/saturn/src/main/java/io/fairspace/saturn/webdav/resources/FileResource.java @@ -16,6 +16,7 @@ import io.milton.http.exceptions.NotFoundException; import io.milton.resource.ReplaceableResource; import lombok.SneakyThrows; +import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.vocabulary.RDF; @@ -40,8 +41,8 @@ public class FileResource extends BaseResource implements io.milton.resource.Fil private boolean singleVersion; @SneakyThrows - public FileResource(DavFactory factory, Resource subject, Access access) { - super(factory, subject, access); + public FileResource(DavFactory factory, Resource subject, Access access, Model userVocabulary) { + super(factory, subject, access, userVocabulary); loadVersion(); } diff --git a/projects/saturn/src/main/resources/application.yaml b/projects/saturn/src/main/resources/application.yaml new file mode 100644 index 0000000000..fe34a41771 --- /dev/null +++ b/projects/saturn/src/main/resources/application.yaml @@ -0,0 +1,120 @@ +server: + port: 8090 + servlet: + context-path: /api + +# Configuration to access the Keycloak server (user lists, user count, etc.) +keycloak: + auth-server-url: ${KEYCLOAK_AUTH_SERVER_URL:http://localhost:5100} + realm: ${KEYCLOAK_REALM:fairspace} + client-id: ${KEYCLOAK_CLIENT_ID:workspace-client} + client-secret: ${KEYCLOAK_CLIENT_SECRET:**********} + super-admin-user: ${KEYCLOAK_SUPER_ADMIN_USER:organisation-admin} + default-user-roles: + - + +# Configuration of Saturn as a resource server +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${keycloak.auth-server-url}/realms/fairspace + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + servlet: + multipart: + max-file-size: 2GB + max-request-size: 2GB + http: + multipart: + enabled: true + +# Configuration for JWT token conversion +jwt: + auth: + converter: + resource-id: workspace-client + principal-attribute: preferred_username + +application: + publicUrl: ${PUBLIC_URL:http://localhost:8080} + jena: + # Base IRI for all metadata entities + metadataBaseIRI: ${METADATA_BASE_IRI:http://localhost/iri/} + # Jena's TDB2 database path + datasetPath: ${DATASET_PATH:data/db} + # Path of the transaction log + transactionLogPath: ${TRANSACTION_LOG_PATH:data/log} + bulkTransactions: ${BULK_TRANSACTIONS:true} + sparql-query-timeout: ${SPARQL_TIMEOUT:30000} + tbd-store-params: + file_mode: "mapped" + block_size: 8193 + block_read_cache_size: 5000 + block_write_cache_size: 1000 + node2nodeid_cache_size: 200000 + nodeid2node_cache_size: 750000 + node_miss_cache_size: 1000 + nodetable: "nodes" + triple_index_primary: "SPO" + triple_indexes: + - "SPO" + - "POS" + - "OSP" + quad_index_primary: "GSPO" + quad_indexes: + - "GSPO" + - "GPOS" + - "GOSP" + - "POSG" + - "OSPG" + - "SPOG" + prefixtable: "prefixes" + prefix_index_primary: "GPU" + prefix_indexes: + - "GPU" + features: + - ExtraStorage + view-database: + enabled: ${VIEW_DATABASE_ENABLED:true} + url: ${VIEW_DATABASE_URL:jdbc:postgresql://localhost:9432/fairspace} + username: ${VIEW_DATABASE_USERNAME:fairspace} + autoCommitEnabled: ${VIEW_DATABASE_AUTO_COMMIT:false} + maxPoolSize: ${VIEW_DATABASE_MAX_POOL_SIZE:10} + connectionTimeout: ${VIEW_DATABASE_CONNECTION_TIMEOUT:1000} + password: ${VIEW_DATABASE_PASSWORD:fairspace} + mvRefreshOnStartRequired: ${MV_REFRESH_ON_START_REQUIRED:true} + cache: + facets: + name: "facets" + autoRefreshEnabled: false + refreshFrequencyInHours: 240 + views: + name: "views" + autoRefreshEnabled: false + refreshFrequencyInHours: 240 + search: + pageRequestTimeout: 10000 + countRequestTimeout: 60000 + maxJoinItems: 50 + web-dav: + # Path of the WebDAV's local blob store + blobStorePath: ${WEBDAV_BLOB_STORE_PATH:data/blobs} + extra-storage: + blobStorePath: "data/extra-blobs" + defaultRootCollections: + - "analysis-export" + + +management: + endpoint: + health: + probes: + enabled: true + health: + readinessState: + enabled: true + livenessState: + enabled: true + server: + port: 8091 diff --git a/projects/saturn/src/main/resources/log4j2.properties b/projects/saturn/src/main/resources/log4j2.properties index 42ead9fb0a..74c11128af 100644 --- a/projects/saturn/src/main/resources/log4j2.properties +++ b/projects/saturn/src/main/resources/log4j2.properties @@ -4,8 +4,6 @@ rootLogger.appenderRef.stdout.ref = stdout # Avoid warn messages from milton standard filter, as they # also appear whenever the user makes a mistake -logger.spark.name = spark.http -logger.spark.level = warn logger.milton.name = io.milton.http logger.milton.level = warn logger.milton-filter.name = io.milton.http.StandardFilter diff --git a/projects/saturn/taxonomies.ttl b/projects/saturn/src/main/resources/taxonomies.ttl similarity index 100% rename from projects/saturn/taxonomies.ttl rename to projects/saturn/src/main/resources/taxonomies.ttl diff --git a/projects/saturn/views.yaml b/projects/saturn/src/main/resources/views.yaml similarity index 100% rename from projects/saturn/views.yaml rename to projects/saturn/src/main/resources/views.yaml diff --git a/projects/saturn/src/main/resources/vocabulary.ttl b/projects/saturn/src/main/resources/vocabulary.ttl new file mode 100644 index 0000000000..85978c23f7 --- /dev/null +++ b/projects/saturn/src/main/resources/vocabulary.ttl @@ -0,0 +1,842 @@ +@prefix fs: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . +@prefix schema: . +@prefix foaf: . +@prefix dash: . +@prefix curie: . + +######################## +### User shapes ### +######################## + +curie:aboutSubject a rdf:Property . +curie:aboutEvent a rdf:Property . +curie:sample a rdf:Property . +curie:analysisType a rdf:Property . +curie:workspaceType a rdf:Property . +curie:principalInvestigator a rdf:Property . +curie:projectCode a rdf:Property . +curie:analysisCode a rdf:Property . +curie:analysisDate a rdf:Property . +curie:platformName a rdf:Property . +curie:analyticPipelineCode a rdf:Property . + +curie:WorkspaceType a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The type of the workspace. " ; + sh:name "Workspace type" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique workspace type label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +## Augmented system class shapes +fs:Workspace sh:property + [ + sh:name "Type" ; + sh:description "Workspace type." ; + sh:maxCount 1 ; + sh:class curie:WorkspaceType ; + sh:path curie:workspaceType + ], + [ + sh:name "Project code" ; + sh:description "Project code related to the workspace." ; + sh:datatype xsd:string ; + dash:singleLine true ; + sh:maxCount 1 ; + sh:path curie:projectCode + ], + [ + sh:name "Principal investigator" ; + sh:description "Name of the PI or team leader." ; + sh:datatype xsd:string ; + dash:singleLine true ; + sh:maxCount 1 ; + sh:path curie:principalInvestigator + ] . + +fs:File sh:property + [ + sh:name "Is about subject" ; + sh:description "Subjects that are featured in this file." ; + sh:class curie:Subject ; + sh:path curie:aboutSubject + ], + [ + sh:name "Is about biological sample" ; + sh:description "Biological samples that are featured in this file." ; + sh:class curie:BiologicalSample ; + sh:path curie:sample + ], + [ + sh:name "Is about tumor pathology event" ; + sh:description "Events that are featured in this file." ; + sh:class curie:TumorPathologyEvent ; + sh:path curie:aboutEvent + ], + [ + sh:name "Type of analysis" ; + sh:description "Type of analysis associated to this file" ; + sh:maxCount 1 ; + sh:class curie:AnalysisType ; + sh:path curie:analysisType + ], + [ + sh:name "Analysis code" ; + sh:description "The analysis code (could be the sequencing run identifier for instance, or the imaging identifier, etc.)" ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path curie:analysisCode + ], + [ + sh:name "Analysis date" ; + sh:description "" ; + sh:datatype xsd:date ; + sh:maxCount 1 ; + sh:path curie:analysisDate + ], + [ + sh:name "Platform name" ; + sh:description "The name of the technology platform on which the analysis has been processed." ; + sh:class curie:TechnologyPlatformName ; + sh:maxCount 1 ; + sh:path curie:platformName + ], + [ + sh:name "Analytic pipeline code" ; + sh:description "The analytic pipeline code (which is helpful for data traceability and comparison)." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path curie:analyticPipelineCode + ] . + +fs:Directory sh:property + [ + sh:name "Is about subject" ; + sh:description "Subjects that are featured in this directory." ; + sh:class curie:Subject ; + sh:path curie:aboutSubject + ], + [ + sh:name "Is about biological sample" ; + sh:description "Biological samples that are featured in this directory." ; + sh:class curie:BiologicalSample ; + sh:path curie:sample + ], + [ + sh:name "Is about tumor pathology event" ; + sh:description "Events that are featured in this directory." ; + sh:class curie:TumorPathologyEvent ; + sh:path curie:aboutEvent + ], + [ + sh:name "Type of analysis" ; + sh:description "Type of analysis associated to this directory" ; + sh:class curie:AnalysisType ; + sh:path curie:analysisType + ] . + +fs:Collection sh:property + [ + sh:name "Is about subject" ; + sh:description "Subjects that are featured in this collection." ; + sh:class curie:Subject ; + sh:path curie:aboutSubject + ], + [ + sh:name "Is about biological sample" ; + sh:description "Biological samples that are featured in this collection." ; + sh:class curie:BiologicalSample ; + sh:path curie:sample + ], + [ + sh:name "Is about tumor pathology event" ; + sh:description "Events that are featured in this collection." ; + sh:class curie:TumorPathologyEvent ; + sh:path curie:aboutEvent + ], + [ + sh:name "Type of analysis" ; + sh:description "Type of analysis associated to this collection" ; + sh:class curie:AnalysisType ; + sh:path curie:analysisType + ] . + +## User class Shapes + +curie:AnalysisType a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The type of analysis." ; + sh:name "Analysis type" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique analysis type label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TechnologyPlatformName a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "" ; + sh:name "Technology platform name" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique technology platform name." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:Gender a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The gender of the subject." ; + sh:name "Gender" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique gender label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:Species a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The species of the subject." ; + sh:name "Species" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique species label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:AvailabilityForResearch a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "Indication whether the subject is available for research." ; + sh:name "Availability for research" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique availability label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:ConsentAnswer a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "Answer type for consent questions." ; + sh:name "Consent answer" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique answer label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:ageAtLastNews a rdf:Property . +curie:ageAtDeath a rdf:Property . +curie:isOfGender a rdf:Property . +curie:isOfSpecies a rdf:Property . +curie:availableForResearch a rdf:Property . +curie:dateOfOpposition a rdf:Property . +curie:reuseClinicalWithGeneticData a rdf:Property . +curie:sampleStorageAndReuse a rdf:Property . +curie:geneticsAnalysis a rdf:Property . + +curie:Subject a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "A subject of research." ; + sh:name "Subject" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Gender" ; + sh:description "The gender of the subject." ; + sh:maxCount 1 ; + sh:class curie:Gender ; + sh:path curie:isOfGender + ], + [ + sh:name "Species" ; + sh:description "The species of the subject." ; + sh:maxCount 1 ; + sh:class curie:Species ; + sh:path curie:isOfSpecies + ], + [ + sh:name "Age at last news" ; + sh:description "The age at last news." ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path curie:ageAtLastNews + ], + [ + sh:name "Age at death" ; + sh:description "The age at death." ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path curie:ageAtDeath + ], + [ + sh:name "Available for research" ; + sh:maxCount 1 ; + sh:class curie:AvailabilityForResearch ; + sh:path curie:availableForResearch + ], + [ + sh:name "Date of opposition" ; + sh:datatype xsd:date ; + sh:maxCount 1 ; + sh:path curie:dateOfOpposition + ], + [ + sh:name "Reuse clinical with genetic data" ; + sh:maxCount 1 ; + sh:class curie:ConsentAnswer ; + sh:path curie:reuseClinicalWithGeneticData + ], + [ + sh:name "Sample storage and reuse" ; + sh:maxCount 1 ; + sh:class curie:ConsentAnswer ; + sh:path curie:sampleStorageAndReuse + ], + [ + sh:name "Genetic analysis" ; + sh:maxCount 1 ; + sh:class curie:ConsentAnswer ; + sh:path curie:geneticAnalysis + ], + [ + sh:name "Label" ; + sh:description "Unique person label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label; + sh:order 0 + ], + [ + sh:name "Description" ; + sh:description "" ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:comment + ], + [ + sh:name "Tumor pathology events" ; + sh:description "Tumor pathology events" ; + sh:path [sh:inversePath curie:eventSubject]; + ], + [ + sh:name "Samples" ; + sh:description "Samples" ; + sh:path [sh:inversePath curie:subject]; + ], + [ + sh:name "Files" ; + sh:description "Linked files" ; + sh:path [sh:inversePath curie:aboutSubject]; + ]. + + +curie:Topography a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The topography of the tumor coded in ICD-10 (subdivision ICD-O-3)." ; + sh:name "Topography" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique topography label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:Morphology a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The morphology of the tumor coded in ICD-10 (subdivision ICD-O-3)." ; + sh:name "Morphology" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique morphology label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:Laterality a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The laterality of the tumor." ; + sh:name "Laterality" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique laterality label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:EventType a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The type of tumor pathology event." ; + sh:name "Event type" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique event type label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TumorGradeType a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The type of grading classification." ; + sh:name "Tumor grade type" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique tumor grade type label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TumorGradeValue a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The grading value." ; + sh:name "Tumor grade value" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique tumor grade value label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TnmT a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The primary tumor size." ; + sh:name "TNM_T" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique TNM_T label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TnmN a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description " The regional lymph nodes." ; + sh:name "TNM_N" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique TNM_N label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:TnmM a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description " Distant metastasis." ; + sh:name "TNM_M" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique TNM_M label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:eventType a rdf:Property . +curie:topography a rdf:Property . +curie:tumorMorphology a rdf:Property . +curie:tumorLaterality a rdf:Property . +curie:yearOfDiagnosis a rdf:Property . +curie:ageAtDiagnosis a rdf:Property . +curie:tumorGradeType a rdf:Property . +curie:tumorGradeValue a rdf:Property . +curie:cTnmT a rdf:Property . +curie:cTnmN a rdf:Property . +curie:cTnmM a rdf:Property . +curie:pTnmT a rdf:Property . +curie:pTnmN a rdf:Property . +curie:pTnmM a rdf:Property . +curie:yTnmT a rdf:Property . +curie:yTnmN a rdf:Property . +curie:yTnmM a rdf:Property . + +curie:isAnalysedBy a rdf:Property . +curie:eventSubject a rdf:Property . +curie:isLinkedTo a rdf:Property . + +curie:TumorPathologyEvent a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "" ; + sh:name "Tumor pathology event" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Topography" ; + sh:description "The topography of the tumor." ; + sh:class curie:Topography ; + sh:path curie:topography + ], + [ + sh:name "Morphology" ; + sh:description "The morphology of the tumor." ; + sh:class curie:Morphology ; + sh:path curie:tumorMorphology + ], + [ + sh:name "Laterality" ; + sh:description "The laterality of the tumor." ; + sh:maxCount 1 ; + sh:class curie:Laterality ; + sh:path curie:tumorLaterality + ], + [ + sh:name "Event type" ; + sh:description "The type of tumor pathology event." ; + sh:maxCount 1 ; + sh:class curie:EventType ; + sh:path curie:eventType + ], + [ + sh:name "Year of diagnosis" ; + sh:description "The diagnosis year of the primary tumor." ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path curie:yearOfDiagnosis + ], + [ + sh:name "Age at diagnosis" ; + sh:description "The age at diagnosis." ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path curie:ageAtDiagnosis + ], + [ + sh:name "Tumor grade type" ; + sh:description "The type of tumor grading classification." ; + sh:class curie:TumorGradeType ; + sh:maxCount 1 ; + sh:path curie:tumorGradeType + ], + [ + sh:name "Tumor grade value" ; + sh:description "The tumor grading value." ; + sh:class curie:TumorGradeValue ; + sh:maxCount 1 ; + sh:path curie:tumorGradeValue + ], + [ + sh:name "cTNM_T" ; + sh:description "The primary tumor size (clinical evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmT ; + sh:path curie:cTnmT + ], + [ + sh:name "cTNM_N" ; + sh:description "The regional lymph nodes (clinical evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmN ; + sh:path curie:cTnmN + ], + [ + sh:name "cTNM_M" ; + sh:description "Distant metastasis (clinical evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmM ; + sh:path curie:cTnmM + ], + [ + sh:name "pTNM_T" ; + sh:description "The primary tumor size (pathological evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmT ; + sh:path curie:pTnmT + ], + [ + sh:name "pTNM_N" ; + sh:description "The regional lymph nodes (pathological evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmN ; + sh:path curie:pTnmN + ], + [ + sh:name "pTNM_M" ; + sh:description "Distant metastasis (pathological evaluation)." ; + sh:maxCount 1 ; + sh:class curie:TnmM ; + sh:path curie:pTnmM + ], + [ + sh:name "yTNM_T" ; + sh:description "The primary tumor size (after treatment)." ; + sh:maxCount 1 ; + sh:class curie:TnmT ; + sh:path curie:yTnmT + ], + [ + sh:name "yTNM_N" ; + sh:description "The regional lymph nodes (after treatment)." ; + sh:maxCount 1 ; + sh:class curie:TnmN ; + sh:path curie:yTnmN + ], + [ + sh:name "yTNM_M" ; + sh:description "Distant metastasis (after treatment)." ; + sh:maxCount 1 ; + sh:class curie:TnmM ; + sh:path curie:yTnmM + ], + [ + sh:name "Event subject" ; + sh:description "The subject associated with this event." ; + sh:class curie:Subject ; + sh:maxCount 1 ; + sh:path curie:eventSubject + ], + [ + sh:name "Label" ; + sh:description "Unique tumor pathology event label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label; + sh:order 0 + ], + [ + sh:name "Description" ; + sh:description "" ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:comment + ], + [ + sh:name "Samples" ; + sh:description "Samples" ; + sh:path [sh:inversePath curie:diagnosis]; + ], + [ + sh:name "Files" ; + sh:description "Linked files" ; + sh:path [sh:inversePath curie:aboutEvent]; + ]. + + +curie:SampleNature a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The sample nature." ; + sh:name "Sample nature" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique sample nature label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:SampleOrigin a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "The sample origin." ; + sh:name "Sample origin" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Label" ; + sh:description "Unique sample origin label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label + ] . + +curie:collectDate a rdf:Property . +curie:tumorCellularity a rdf:Property . +curie:isOfNature a rdf:Property . +curie:parentIsOfNature a rdf:Property . +curie:hasOrigin a rdf:Property . +curie:subject a rdf:Property . +curie:diagnosis a rdf:Property . +curie:isChildOf a rdf:Property . + +curie:BiologicalSample a rdfs:Class, sh:NodeShape ; + sh:closed false ; + sh:description "" ; + sh:name "Biological sample" ; + sh:ignoredProperties ( rdf:type owl:sameAs ) ; + sh:property + [ + sh:name "Collect date" ; + sh:description "The collect date of the biological sample." ; + sh:datatype xsd:date ; + sh:maxCount 1 ; + sh:path curie:collectDate + ], + [ + sh:name "Tumor cellularity" ; + sh:description "The percentage of tumor cells in the biological sample (pathological measure)." ; + sh:datatype xsd:integer ; + sh:maxCount 1 ; + sh:path curie:tumorCellularity + ], + [ + sh:name "Topography" ; + sh:description "The topography of the sample." ; + sh:maxCount 1 ; + sh:class curie:Topography ; + sh:path curie:topography + ], + [ + sh:name "Sample nature" ; + sh:description "The sample nature." ; + sh:maxCount 1 ; + sh:class curie:SampleNature ; + sh:path curie:isOfNature + ], + [ + sh:name "Parent sample nature" ; + sh:description "Natures of parent samples." ; + sh:class curie:SampleNature ; + sh:path curie:parentIsOfNature + ], + [ + sh:name "Sample origin" ; + sh:description "The sample origin." ; + sh:maxCount 1 ; + sh:class curie:SampleOrigin ; + sh:path curie:hasOrigin ; + ], + [ + sh:name "Subject" ; + sh:description "The subject associated with this sample." ; + sh:class curie:Subject ; + sh:maxCount 1 ; + sh:path curie:subject + ], + [ + sh:name "Diagnosis" ; + sh:description "The diagnosing tumor pathology event." ; + sh:class curie:TumorPathologyEvent ; + sh:maxCount 1 ; + sh:path curie:diagnosis + ], + [ + sh:name "Sample is child sample of" ; + sh:description "The biological sample has been extracted from this parent sample." ; + sh:class curie:BiologicalSample ; + sh:path curie:isChildOf + ], + [ + sh:name "Label" ; + sh:description "Unique biological sample label." ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + dash:singleLine true ; + fs:importantProperty true ; + sh:path rdfs:label; + sh:order 0 + ], + [ + sh:name "Description" ; + sh:description "" ; + sh:datatype xsd:string ; + sh:maxCount 1 ; + sh:path rdfs:comment + ], + [ + sh:name "Child samples" ; + sh:description "Child samples" ; + sh:path [sh:inversePath curie:isChildOf]; + ], + [ + sh:name "Files" ; + sh:description "Linked files" ; + sh:path [sh:inversePath curie:sample]; + ]. diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/PostgresAwareTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/PostgresAwareTest.java index 74c4d45d99..3a817b12ae 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/PostgresAwareTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/PostgresAwareTest.java @@ -1,9 +1,16 @@ package io.fairspace.saturn; +import javax.sql.DataSource; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import org.junit.AfterClass; import org.junit.BeforeClass; import org.testcontainers.containers.PostgreSQLContainer; +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.ViewDatabaseProperties; + public class PostgresAwareTest { protected static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:15-alpine"); @@ -17,4 +24,37 @@ public static void beforeAll() { public static void afterAll() { postgres.stop(); } + + protected ViewDatabaseProperties buildViewDatabaseConfig() { + var viewDatabase = new ViewDatabaseProperties(); + viewDatabase.setUrl(postgres.getJdbcUrl()); + viewDatabase.setUsername(postgres.getUsername()); + viewDatabase.setPassword(postgres.getPassword()); + viewDatabase.setMaxPoolSize(5); + return viewDatabase; + } + + protected SearchProperties buildSearchProperties() { + SearchProperties searchProperties = new SearchProperties(); + searchProperties.setCountRequestTimeout(60000); + searchProperties.setPageRequestTimeout(10000); + searchProperties.setMaxJoinItems(50); + return searchProperties; + } + + public DataSource getDataSource(ViewDatabaseProperties viewDatabaseProperties) { + var databaseConfig = getHikariConfig(viewDatabaseProperties); + return new HikariDataSource(databaseConfig); + } + + private HikariConfig getHikariConfig(ViewDatabaseProperties viewDatabaseProperties) { + var databaseConfig = new HikariConfig(); + databaseConfig.setJdbcUrl(viewDatabaseProperties.getUrl()); + databaseConfig.setUsername(viewDatabaseProperties.getUsername()); + databaseConfig.setPassword(viewDatabaseProperties.getPassword()); + databaseConfig.setAutoCommit(viewDatabaseProperties.isAutoCommitEnabled()); + databaseConfig.setConnectionTimeout(viewDatabaseProperties.getConnectionTimeout()); + databaseConfig.setMaximumPoolSize(viewDatabaseProperties.getMaxPoolSize()); + return databaseConfig; + } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/TestUtils.java b/projects/saturn/src/test/java/io/fairspace/saturn/TestUtils.java index 73e10bd26b..9a94e7700b 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/TestUtils.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/TestUtils.java @@ -3,28 +3,39 @@ import java.io.File; import java.io.IOException; import java.time.Instant; +import java.util.HashMap; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import jakarta.servlet.http.HttpServletRequest; import org.apache.jena.rdf.model.Model; -import org.eclipse.jetty.server.Authentication; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.UserIdentity; -import org.keycloak.KeycloakPrincipal; -import org.keycloak.KeycloakSecurityContext; -import org.keycloak.representations.AccessToken; - -import io.fairspace.saturn.config.ViewsConfig; +import org.jetbrains.annotations.NotNull; +import org.mockito.Mockito; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; + +import io.fairspace.saturn.auth.RequestContext; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.services.users.User; -import static io.fairspace.saturn.auth.RequestContext.setCurrentRequest; - import static java.time.Instant.now; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; public class TestUtils { + + public static final String ADMIN = "admin"; + + public static final String USER = "user"; + public static void ensureRecentInstant(Instant instant) { assertNotNull(instant); assertTrue(instant.isAfter(now().minusSeconds(1))); @@ -39,20 +50,22 @@ public static Model contains(Model m) { return argThat(a -> a.containsAll(m)); } - public static Authentication.User mockAuthentication(String username) { - var auth = mock(Authentication.User.class); - var identity = mock(UserIdentity.class, withSettings().lenient()); - when(auth.getUserIdentity()).thenReturn(identity); - var principal = mock(KeycloakPrincipal.class, withSettings().lenient()); - when(identity.getUserPrincipal()).thenReturn(principal); - when(principal.getName()).thenReturn(username); - var context = mock(KeycloakSecurityContext.class, withSettings().lenient()); - when(principal.getKeycloakSecurityContext()).thenReturn(context); - var token = mock(AccessToken.class, withSettings().lenient()); - when(context.getToken()).thenReturn(token); - when(token.getSubject()).thenReturn(username); - when(token.getName()).thenReturn("fullname"); - return auth; + public static void mockAuthentication(String username) { + // this is a trick for tests to pass security context to other threads + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); + // Create a mock SecurityContext + var mockSecurityContext = Mockito.mock(SecurityContext.class); + + // Create a mock Authentication object + var mockAuthentication = Mockito.mock(Authentication.class); + + // Set the mocked authentication into the security context + lenient().when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + + Jwt mockJwt = getMockedJwt(username); + lenient().when(mockAuthentication.getPrincipal()).thenReturn(mockJwt); + // Set the mocked SecurityContext in the SecurityContextHolder + SecurityContextHolder.setContext(mockSecurityContext); } public static User createTestUser(String username, boolean isAdmin) { @@ -60,27 +73,70 @@ public static User createTestUser(String username, boolean isAdmin) { user.setId(username); user.setUsername(username); user.setName(username); - user.setIri(SparqlUtils.generateMetadataIri(username)); + user.setIri(SparqlUtils.generateMetadataIriFromId(username)); user.setAdmin(isAdmin); return user; } + // todo: implement this method with Spring Security + public static void setupRequestContext(final String username) { + var request = mock(HttpServletRequest.class); + RequestContext.setCurrentRequest(request); + RequestContext.setCurrentUserStringUri( + SparqlUtils.generateMetadataIriFromId(username).getURI()); + // this is a trick for tests to pass security context to other threads + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); + // Create a mock SecurityContext + var mockSecurityContext = Mockito.mock(SecurityContext.class); + + // Create a mock Authentication object + var mockAuthentication = Mockito.mock(Authentication.class); + + // Set the mocked authentication into the security context + lenient().when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + + Jwt mockJwt = getMockedJwt(username); + // just to set static private metadata base IRI + new JenaProperties("http://localhost/iri/", new StoreParamsProperties()); + lenient().when(mockAuthentication.getPrincipal()).thenReturn(mockJwt); + // Set the mocked SecurityContext in the SecurityContextHolder + SecurityContextHolder.setContext(mockSecurityContext); + } + + private static @NotNull Jwt getMockedJwt(String username) { + var claims = new HashMap(); + claims.put("preferred_username", "fullname"); + claims.put("sub", username); + claims.put("email", "johndoe@example.com"); + claims.put("name", "fullname"); + return Jwt.withTokenValue("mock-token") + .header("alg", "RS256") + .claim("preferred_username", claims.get("preferred_username")) + .claim("sub", claims.get("sub")) + .claim("email", claims.get("email")) + .claim("name", claims.get("name")) + .build(); + } + public static void setupRequestContext() { - var request = mock(Request.class); - setCurrentRequest(request); - var auth = mockAuthentication("userid"); - when(request.getAuthentication()).thenReturn(auth); + setupRequestContext(USER); + } + + public static ObjectMapper getYamlObjectMapper() { + return new ObjectMapper(new YAMLFactory()); } - public static ViewsConfig loadViewsConfig(String path) { + public static ViewsProperties loadViewsConfig(String path) { var settingsFile = new File(path); if (settingsFile.exists()) { try { - return ViewsConfig.MAPPER.readValue(settingsFile, ViewsConfig.class); + ViewsProperties viewsProperties = getYamlObjectMapper().readValue(settingsFile, ViewsProperties.class); + viewsProperties.init(); + return viewsProperties; } catch (IOException e) { throw new RuntimeException("Error loading search configuration", e); } } - return new ViewsConfig(); + return new ViewsProperties(); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/config/ServicesTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/config/ServicesTest.java deleted file mode 100644 index d643cf8570..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/config/ServicesTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.fairspace.saturn.config; - -import org.apache.jena.query.Dataset; -import org.apache.jena.query.DatasetFactory; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; - -import io.fairspace.saturn.webdav.resources.ExtraStorageRootResource; - -import static org.junit.Assert.*; - -public class ServicesTest { - private Dataset dataset = DatasetFactory.create(); - private Config config = new Config(); - private ViewsConfig viewsConfig = new ViewsConfig(); - private Services svc; - - @Rule - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - - @Before - public void before() { - environmentVariables.set("KEYCLOAK_CLIENT_SECRET", "secret"); - svc = new Services(config, viewsConfig, dataset, null); - } - - @Test - public void getConfig() { - assertEquals(config, svc.getConfig()); - } - - @Test - public void getTransactions() { - assertNotNull(svc.getTransactions()); - } - - @Test - public void getUserService() { - assertNotNull(svc.getUserService()); - } - - @Test - public void getPermissionsService() { - assertNotNull(svc.getMetadataPermissions()); - } - - @Test - public void getMetadataService() { - assertNotNull(svc.getMetadataService()); - } - - @Test - public void getExtraDavServlet() { - assertNotNull(svc.getExtraBlobStore()); - assertNotNull(svc.getExtraDavFactory()); - assertNotNull(svc.getExtraDavServlet()); - assertTrue(svc.getExtraDavFactory().root instanceof ExtraStorageRootResource); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java deleted file mode 100644 index c8acc65e88..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/config/SparkFilterFactoryTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.fairspace.saturn.config; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import static org.junit.Assert.assertNotNull; - -@RunWith(MockitoJUnitRunner.class) -public class SparkFilterFactoryTest { - @Mock - private Services svc; - - @Test - public void itCreatesAFilter() { - assertNotNull(SparkFilterFactory.createSparkFilter("/some/path", svc, new Config())); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java new file mode 100644 index 0000000000..bc11b1fbab --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/BaseControllerTest.java @@ -0,0 +1,31 @@ +package io.fairspace.saturn.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import io.fairspace.saturn.auth.JwtAuthConverterProperties; +import io.fairspace.saturn.services.IRIModule; + +@ImportAutoConfiguration(exclude = {SecurityAutoConfiguration.class, OAuth2ResourceServerAutoConfiguration.class}) +@Import(BaseControllerTest.CustomObjectMapperConfig.class) +public class BaseControllerTest { + + @MockBean + private JwtAuthConverterProperties jwtAuthConverterProperties; + + @TestConfiguration + static class CustomObjectMapperConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new IRIModule()) + .findAndRegisterModules(); // Automatically registers JavaTimeModule, etc. + } + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java new file mode 100644 index 0000000000..4c97278771 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/FeaturesControllerTest.java @@ -0,0 +1,41 @@ +package io.fairspace.saturn.controller; + +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.config.enums.Feature; +import io.fairspace.saturn.config.properties.FeatureProperties; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(FeaturesController.class) +class FeaturesControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private FeatureProperties featureProperties; + + @Test + void testGetFeatures() throws Exception { + // Mock the response from featureProperties + Set features = Set.of(Feature.ExtraStorage); + when(featureProperties.getFeatures()).thenReturn(features); + + // Perform GET request and verify the response + mockMvc.perform(get("/features/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("[\"ExtraStorage\"]")); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java new file mode 100644 index 0000000000..0ed41698a7 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MaintenanceControllerTest.java @@ -0,0 +1,66 @@ +package io.fairspace.saturn.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.maintenance.MaintenanceService; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MaintenanceController.class) +class MaintenanceControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MaintenanceService maintenanceService; + + @Test + void testStartReindex() throws Exception { + doNothing().when(maintenanceService).startRecreateIndexTask(); + + mockMvc.perform(post("/maintenance/reindex").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); // Expect 204 No Content + verify(maintenanceService).startRecreateIndexTask(); + } + + @Test + void testCompactRdfStorage() throws Exception { + doNothing().when(maintenanceService).compactRdfStorageTask(); + + mockMvc.perform(post("/maintenance/compact").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); // Expect 204 No Content + verify(maintenanceService).compactRdfStorageTask(); + } + + @Test + void testGetStatusActive() throws Exception { + when(maintenanceService.active()).thenReturn(true); + + mockMvc.perform(get("/maintenance/status").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect(content().string("active")); // Expect content "active" + verify(maintenanceService).active(); + } + + @Test + void testGetStatusInactive() throws Exception { + when(maintenanceService.active()).thenReturn(false); + + mockMvc.perform(get("/maintenance/status").accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect(content().string("inactive")); // Expect content "inactive" + verify(maintenanceService).active(); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java new file mode 100644 index 0000000000..420faa0883 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/MetadataControllerTest.java @@ -0,0 +1,119 @@ +package io.fairspace.saturn.controller; + +import org.apache.jena.rdf.model.Model; +import org.apache.jena.rdf.model.ModelFactory; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.metadata.MetadataService; + +import static io.fairspace.saturn.controller.enums.CustomMediaType.TEXT_TURTLE; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(MetadataController.class) +public class MetadataControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MetadataService metadataService; + + @Test + public void testGetMetadata() throws Exception { + Model mockModel = ModelFactory.createDefaultModel(); // Create an empty Jena model for testing + mockModel.add( + mockModel.createResource("http://example.com"), + mockModel.createProperty("http://example.com/property"), + "test-value"); + Mockito.when(metadataService.get(eq("http://example.com"), eq(false))).thenReturn(mockModel); + + mockMvc.perform(get("/metadata/") + .param("subject", "http://example.com") + .param("withValueProperties", "false") + .header("Accept", TEXT_TURTLE)) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("http://example.com"))) + .andExpect(header().string("Content-Type", TEXT_TURTLE + ";charset=UTF-8")); + } + + @Test + public void testPutMetadata() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "value" . + """; + + mockMvc.perform(put("/metadata/").content(body).contentType(TEXT_TURTLE).param("doViewsUpdate", "true")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).put(any(Model.class), eq(true)); + } + + @Test + public void testPatchMetadata() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "updated-value" . + """; + + mockMvc.perform(patch("/metadata/") + .content(body) + .contentType(TEXT_TURTLE) + .param("doViewsUpdate", "false")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).patch(any(Model.class), eq(false)); + } + + @Test + public void testDeleteMetadataBySubject() throws Exception { + Mockito.when(metadataService.softDelete(any())).thenReturn(true); + + mockMvc.perform(delete("/metadata/").param("subject", "http://example.com")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).softDelete(any()); + } + + @Test + public void testDeleteMetadataByModel() throws Exception { + String body = + """ + @prefix ex: . + ex:subject ex:property "value" . + """; + + mockMvc.perform(delete("/metadata/") + .content(body) + .contentType(TEXT_TURTLE) + .param("doViewsUpdate", "true")) + .andExpect(status().isNoContent()); + + Mockito.verify(metadataService).delete(any(Model.class), eq(true)); + } + + @Test + public void testDeleteMetadataSubjectNotFound() throws Exception { + Mockito.when(metadataService.softDelete(any())).thenReturn(false); + + mockMvc.perform(delete("/metadata/").param("subject", "http://example.com")) + .andExpect(status().isBadRequest()); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java new file mode 100644 index 0000000000..64e41f8688 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/SearchControllerTest.java @@ -0,0 +1,125 @@ +package io.fairspace.saturn.controller; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.controller.dto.SearchResultDto; +import io.fairspace.saturn.controller.dto.SearchResultsDto; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; +import io.fairspace.saturn.controller.dto.request.LookupSearchRequest; +import io.fairspace.saturn.services.search.FileSearchService; +import io.fairspace.saturn.services.search.SearchService; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(SearchController.class) +class SearchControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SearchService searchService; + + @MockBean + private FileSearchService fileSearchService; + + @Test + void testSearchFiles() throws Exception { + var mockResults = List.of( + SearchResultDto.builder() + .id("file1.txt") + .label("File 1") + .type("text") + .comment("First file") + .build(), + SearchResultDto.builder() + .id("file2.txt") + .label("File 2") + .type("text") + .comment("Second file") + .build()); + when(fileSearchService.searchFiles(any(FileSearchRequest.class))).thenReturn(mockResults); + + mockMvc.perform( + post("/search/files") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "query": "test query", + "parentIRI": "parent/iri" + } + """)) + .andExpect(status().isOk()) + .andExpect( + content() + .json( + """ + { + "results": [ + {"id": "file1.txt", "label": "File 1", "type": "text", "comment": "First file"}, + {"id": "file2.txt", "label": "File 2", "type": "text", "comment": "Second file"} + ], + "query": "test query" + } + """)); + } + + @Test + void testLookupSearch() throws Exception { + var mockResults = List.of( + SearchResultDto.builder() + .id("file1.txt") + .label("File 1") + .type("text") + .comment("First file") + .build(), + SearchResultDto.builder() + .id("file2.txt") + .label("File 2") + .type("text") + .comment("Second file") + .build()); + var resultsDTO = SearchResultsDto.builder() + .results(mockResults) + .query("test query") + .build(); + when(searchService.getLookupSearchResults(any(LookupSearchRequest.class))) + .thenReturn(resultsDTO); + + mockMvc.perform( + post("/search/lookup") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "query": "lookup query", + "resourceType": "resourceType" + } + """)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + { + "results": [ + {"id": "file1.txt", "label": "File 1", "type": "text", "comment": "First file"}, + {"id": "file2.txt", "label": "File 2", "type": "text", "comment": "Second file"} + ], + "query": "test query" + } + """)); // Verify JSON response + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java new file mode 100644 index 0000000000..70e7587cac --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/UserControllerTest.java @@ -0,0 +1,145 @@ +package io.fairspace.saturn.controller; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.users.User; +import io.fairspace.saturn.services.users.UserRolesUpdate; +import io.fairspace.saturn.services.users.UserService; + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +class UserControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private UserService service; + + @Test + void testGetUsers() throws Exception { + var user1 = createTestUser("1", "User One", "user1@example.com", "user1", true, false); + var user2 = createTestUser("2", "User Two", "user2@example.com", "user2", false, true); + var users = List.of(user1, user2); + when(service.getUsers()).thenReturn(users); + + mockMvc.perform(get("/users/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + [ + { + "id": "1", + "name": "User One", + "email": "user1@example.com", + "username": "user1", + "isSuperadmin": true, + "isAdmin": false, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + }, + { + "id": "2", + "name": "User Two", + "email": "user2@example.com", + "username": "user2", + "isSuperadmin": false, + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + } + ] + """)); + } + + @Test + void testUpdateUserRoles() throws Exception { + UserRolesUpdate update = new UserRolesUpdate(); + update.setId("1"); + update.setAdmin(true); + update.setCanViewPublicMetadata(true); + update.setCanViewPublicData(false); + update.setCanAddSharedMetadata(true); + update.setCanQueryMetadata(false); + + doNothing().when(service).update(update); + + mockMvc.perform( + patch("/users/") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "id": "1", + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": false + } + """)) + .andExpect(status().isNoContent()); + } + + @Test + void testGetCurrentUser() throws Exception { + var currentUser = createTestUser("1", "Current User", "currentuser@example.com", "currentuser", true, true); + + when(service.currentUser()).thenReturn(currentUser); + + mockMvc.perform(get("/users/current").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) // Expect 200 OK + .andExpect( + content() + .json( + """ + { + "id": "1", + "name": "Current User", + "email": "currentuser@example.com", + "username": "currentuser", + "isSuperadmin": true, + "isAdmin": true, + "canViewPublicMetadata": true, + "canViewPublicData": false, + "canAddSharedMetadata": true, + "canQueryMetadata": true + } + """)); + } + + private User createTestUser( + String id, String name, String email, String username, boolean superadmin, boolean admin) { + User user = new User(); + user.setId(id); + user.setName(name); + user.setEmail(email); + user.setUsername(username); + user.setSuperadmin(superadmin); + user.setAdmin(admin); + user.setCanViewPublicMetadata(true); + user.setCanViewPublicData(false); + user.setCanAddSharedMetadata(true); + user.setCanQueryMetadata(true); + return user; + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java new file mode 100644 index 0000000000..37a733c37a --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/ViewControllerTest.java @@ -0,0 +1,143 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.controller.dto.CountDto; +import io.fairspace.saturn.controller.dto.FacetDto; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.ViewDto; +import io.fairspace.saturn.controller.dto.ViewPageDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; +import io.fairspace.saturn.services.views.QueryService; +import io.fairspace.saturn.services.views.ViewService; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(ViewController.class) +public class ViewControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ViewService viewService; + + @MockBean(name = "queryService") + private QueryService queryService; + + @Autowired + private ObjectMapper objectMapper; + + @Test + public void testGetViewsSuccess() throws Exception { + var viewDto = new ViewDto("view1", "View 1", List.of(), 100L); + + when(viewService.getViews()).thenReturn(List.of(viewDto)); + + mockMvc.perform(get("/views/").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.views", hasSize(1))) + .andExpect(jsonPath("$.views[0].name", is("view1"))) + .andExpect(jsonPath("$.views[0].title", is("View 1"))) + .andExpect(jsonPath("$.views[0].columns", hasSize(0))) + .andExpect(jsonPath("$.views[0].maxDisplayCount", is(100))); // Max display count is null + } + + @Test + public void testGetViewDataSuccess() throws Exception { + // Mock request body and response + var viewRequest = new ViewRequest(); + viewRequest.setView("view1"); + viewRequest.setPage(1); + viewRequest.setSize(10); + + var row = Map.of("view1_column1", Set.of(new ValueDto("label1", "value1"))); + var viewPageDto = ViewPageDto.builder() + .rows(List.of(row)) + .hasNext(false) + .timeout(false) + .totalCount(100L) + .totalPages(10L) + .build(); + + when(queryService.retrieveViewPage(viewRequest)).thenReturn(viewPageDto); + + mockMvc.perform(post("/views/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(viewRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.rows", hasSize(1))) + .andExpect(jsonPath("$.rows[0]['view1_column1']", hasSize(1))) + .andExpect(jsonPath("$.rows[0]['view1_column1'][0].value", is("value1"))) + .andExpect(jsonPath("$.hasNext", is(false))) + .andExpect(jsonPath("$.timeout", is(false))) + .andExpect(jsonPath("$.totalCount", is(100))) + .andExpect(jsonPath("$.totalPages", is(10))); + } + + @Test + public void testGetFacetsSuccess() throws Exception { + // Mock data for getFacets + var facetDto = new FacetDto("facet1", "Facet 1", ViewsProperties.ColumnType.Set, List.of(), null, null, null); + + when(viewService.getFacets()).thenReturn(List.of(facetDto)); + + mockMvc.perform(get("/views/facets").contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.facets", hasSize(1))) + .andExpect(jsonPath("$.facets[0].name", is("facet1"))) + .andExpect(jsonPath("$.facets[0].title", is("Facet 1"))) + .andExpect(jsonPath( + "$.facets[0].type", is(ViewsProperties.ColumnType.Set.getName()))); // Empty options list + } + + @Test + public void testCountSuccess() throws Exception { + // Mock request body and response + var countRequest = new CountRequest(); + countRequest.setView("view1"); + countRequest.setFilters(List.of()); + + var countDto = new CountDto(100, false); + + when(queryService.count(countRequest)).thenReturn(countDto); + + mockMvc.perform(post("/views/count") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(countRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.count", is(100))) + .andExpect(jsonPath("$.timeout", is(false))); + } + + @Test + public void testGetViewDataValidationFailure() throws Exception { + // Test validation error (e.g., invalid request body) + var invalidRequestBody = new ViewRequest(); + invalidRequestBody.setPage(0); // Invalid page (must be >= 1) + invalidRequestBody.setSize(0); // Invalid size (must be >= 1) + + mockMvc.perform(post("/views/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidRequestBody))) + .andExpect(status().isBadRequest()); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java new file mode 100644 index 0000000000..6c3ddecb56 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/VocabularyControllerTest.java @@ -0,0 +1,37 @@ +package io.fairspace.saturn.controller; + +import org.apache.jena.rdf.model.Model; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MockMvc; + +import static org.apache.jena.riot.RDFDataMgr.loadModel; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(VocabularyController.class) +class VocabularyControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @TestConfiguration + static class CustomVocabularyConfig { + @Bean + public Model vocabulary() { + return loadModel("vocabulary.ttl"); + } + } + + @Test + void testGetVocabularyWithJsonLd() throws Exception { + mockMvc.perform(get("/vocabulary/").header(HttpHeaders.ACCEPT, "application/ld+json")) + .andExpect(status().isOk()) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, "application/ld+json")); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java new file mode 100644 index 0000000000..f9f859eccb --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/WorkspaceControllerTest.java @@ -0,0 +1,113 @@ +package io.fairspace.saturn.controller; + +import java.util.List; +import java.util.Map; + +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceRole; +import io.fairspace.saturn.services.workspaces.WorkspaceService; + +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(WorkspaceController.class) +class WorkspaceControllerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private WorkspaceService workspaceService; + + @Test + void createWorkspace_shouldReturnCreatedWorkspace() throws Exception { + Workspace workspace = new Workspace(); + workspace.setCode("WS001"); + + when(workspaceService.createWorkspace(any(Workspace.class))).thenReturn(workspace); + + mockMvc.perform(put("/workspaces/") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"code\": \"WS001\", \"title\": \"New Workspace\"}")) + .andExpect(status().isOk()) // Use isCreated() if HTTP 201 Created is implemented + .andExpect(jsonPath("$.code").value("WS001")); + } + + @Test + void listWorkspaces_shouldReturnListOfWorkspaces() throws Exception { + Workspace workspace = new Workspace(); + workspace.setCode("WS001"); + + when(workspaceService.listWorkspaces()).thenReturn(List.of(workspace)); + + mockMvc.perform(get("/workspaces/").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].code").value("WS001")); + } + + @Test + void deleteWorkspace_shouldDeleteWorkspace() throws Exception { + String workspaceUri = "http://example.com/workspace/1"; + + mockMvc.perform(delete("/workspaces/").param("workspace", workspaceUri)).andExpect(status().isNoContent()); + + Mockito.verify(workspaceService).deleteWorkspace(NodeFactory.createURI(workspaceUri)); + } + + @Test + void getUsers_shouldReturnWorkspaceUsers() throws Exception { + String workspaceUri = "http://example.com/workspace/1"; + var users = Map.of(NodeFactory.createURI("http://example.com/user/1"), WorkspaceRole.Member); + + when(workspaceService.getUsers(any())).thenReturn(users); + + mockMvc.perform(get("/workspaces/users/").param("workspace", workspaceUri)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$['http://example.com/user/1']").value("Member")); + } + + @Test + void setUserRole_shouldUpdateUserRole() throws Exception { + mockMvc.perform( + patch("/workspaces/users/") + .contentType(MediaType.APPLICATION_JSON) + .content( + "{\"workspace\": \"http://example.com/workspace/1\", \"user\": \"http://example.com/user/1\", \"role\": \"Manager\"}")) + .andExpect(status().isNoContent()); + + // Use ArgumentCaptor to capture the arguments passed to the method + ArgumentCaptor workspaceCaptor = ArgumentCaptor.forClass(Node.class); + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(Node.class); + ArgumentCaptor roleCaptor = ArgumentCaptor.forClass(WorkspaceRole.class); + + // Verify that setUserRole was called once and capture the arguments + Mockito.verify(workspaceService, times(1)) + .setUserRole(workspaceCaptor.capture(), userCaptor.capture(), roleCaptor.capture()); + + // Now you can assert that the captured arguments are what you expect + assertEquals(NodeFactory.createURI("http://example.com/workspace/1"), workspaceCaptor.getValue()); + assertEquals(NodeFactory.createURI("http://example.com/user/1"), userCaptor.getValue()); + assertEquals(WorkspaceRole.Manager, roleCaptor.getValue()); // Make sure the role is Manager + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java new file mode 100644 index 0000000000..7b8c8b231b --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/GlobalExceptionHandlerTest.java @@ -0,0 +1,135 @@ +package io.fairspace.saturn.controller.exception; + +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.test.web.servlet.MockMvc; + +import io.fairspace.saturn.controller.BaseControllerTest; +import io.fairspace.saturn.services.metadata.validation.ValidationException; +import io.fairspace.saturn.services.metadata.validation.Violation; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest({GlobalExceptionHandler.class, TestController.class}) +public class GlobalExceptionHandlerTest extends BaseControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TestController.TestInnerClass testInnerClass; + + @Test + public void testHandleConstraintViolationException() throws Exception { + // Mocking a ConstraintViolationException with a couple of violations + ConstraintViolation violation1 = Mockito.mock(ConstraintViolation.class); + ConstraintViolation violation2 = Mockito.mock(ConstraintViolation.class); + when(violation1.getMessage()).thenReturn("Violation 1"); + when(violation2.getMessage()).thenReturn("Violation 2"); + Set> violations = Set.of(violation1, violation2); + ConstraintViolationException exception = new ConstraintViolationException(violations); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": "Violations: Violation 1; Violation 2" + } + """)); + } + + @Test + public void testHandleValidationException() throws Exception { + // Mocking a ValidationException with a violation + Set violations = Set.of(new Violation("Invalid value", "subject", "predicate", "value")); + ValidationException exception = new ValidationException(violations); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": [ + { + "message": "Invalid value", + "subject": "subject", + "predicate": "predicate", + "value": "value" + } + ] + } + """)); + } + + @Test + public void testHandleIllegalArgumentException() throws Exception { + // Mocking an IllegalArgumentException + IllegalArgumentException exception = new IllegalArgumentException("Invalid argument"); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isBadRequest()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 400, + "message": "Validation Error", + "details": "Invalid argument" + } + """)); + } + + @Test + public void testHandleAccessDeniedException() throws Exception { + // Mocking an AccessDeniedException + AccessDeniedException exception = new AccessDeniedException("Access denied"); + + doThrow(exception).when(testInnerClass).method(); // Simulating the exception + + mockMvc.perform(get("/test")) + .andExpect(status().isForbidden()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content() + .json( + """ + { + "status": 403, + "message": "Access Denied", + "details": null + } + """)); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java new file mode 100644 index 0000000000..97151333f3 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/exception/TestController.java @@ -0,0 +1,23 @@ +package io.fairspace.saturn.controller.exception; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class TestController { + + private final TestInnerClass testInnerClass; + + @GetMapping("/test") + public void testMethod() { + testInnerClass.method(); + } + + @Component + public static class TestInnerClass { + public void method() {} + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java new file mode 100644 index 0000000000..a2d2887c11 --- /dev/null +++ b/projects/saturn/src/test/java/io/fairspace/saturn/controller/validation/IriValidatorTest.java @@ -0,0 +1,56 @@ +package io.fairspace.saturn.controller.validation; + +import jakarta.validation.ConstraintValidatorContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class IriValidatorTest { + + @InjectMocks + private IriValidator iriValidator; + + @Mock + private ConstraintValidatorContext constraintValidatorContext; + + @Mock + private ConstraintValidatorContext.ConstraintViolationBuilder constraintViolationBuilder; + + @Test + void testValidIri() { + String validIri = "http://example.com/resource/123"; + + // Test that a valid IRI returns true + assertTrue(iriValidator.isValid(validIri, constraintValidatorContext)); + + // No violations should be added for valid IRI + verify(constraintValidatorContext, never()).buildConstraintViolationWithTemplate(anyString()); + } + + @Test + void testInvalidIri() { + String invalidIri = " fd "; + + // Set up mocking behavior for invalid IRI case + when(constraintValidatorContext.getDefaultConstraintMessageTemplate()).thenReturn("Invalid IRI: %s"); + when(constraintValidatorContext.buildConstraintViolationWithTemplate(anyString())) + .thenReturn(constraintViolationBuilder); + + // Test that an invalid IRI returns false + assertFalse(iriValidator.isValid(invalidIri, constraintValidatorContext)); + + // Verify that a violation was added + verify(constraintValidatorContext).disableDefaultConstraintViolation(); + verify(constraintValidatorContext).buildConstraintViolationWithTemplate(anyString()); + } +} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java index 8fd86ed75c..902b9e9d54 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/SaturnDatasetFactoryTest.java @@ -10,11 +10,12 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.services.maintenance.MaintenanceService; import io.fairspace.saturn.services.views.ViewStoreClientFactory; -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; - import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -31,7 +32,7 @@ public void testIsRestoreNeededForEmptyDirectory() throws IOException { } @Test - public void testIsRestoreNeededForNonExistingDirectory() throws IOException { + public void testIsRestoreNeededForNonExistingDirectory() { File nonExistentDirectory = new File(testFolder.getRoot(), "non-existent-directory"); assertTrue(SaturnDatasetFactory.isRestoreNeeded(nonExistentDirectory)); } @@ -54,7 +55,11 @@ public void testIsRestoreIfNoDataDirectoryIsPresent() throws IOException { public void testUnwrappingDatasetGraphIsOfRightType() { // give var viewStoreClientFactory = mock(ViewStoreClientFactory.class); - var ds = SaturnDatasetFactory.connect(CONFIG.jena, viewStoreClientFactory); + var jenaProperties = new JenaProperties("", new StoreParamsProperties()); + jenaProperties.setDatasetPath(new File("data/db")); + jenaProperties.setTransactionLogPath(new File("data/log")); + var viewProperties = new ViewsProperties(); + var ds = SaturnDatasetFactory.connect(viewProperties, jenaProperties, viewStoreClientFactory, ""); var dataSetGraph = MaintenanceService.unwrap(ds.asDatasetGraph()); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/dao/DAOTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/dao/DAOTest.java index 13dbfd7be1..644d7745e9 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/dao/DAOTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/dao/DAOTest.java @@ -14,16 +14,25 @@ import org.junit.Before; import org.junit.Test; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; + import static io.fairspace.saturn.TestUtils.ensureRecentInstant; import static io.fairspace.saturn.TestUtils.setupRequestContext; -import static io.fairspace.saturn.config.ConfigLoader.CONFIG; import static io.fairspace.saturn.util.ValidationUtils.validateIRI; import static java.time.Instant.now; import static org.apache.jena.graph.NodeFactory.createURI; import static org.apache.jena.query.DatasetFactory.createTxnMem; -import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.junit.Assert.*; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class DAOTest { private Dataset dataset; @@ -39,6 +48,8 @@ public void before() { entity = new Entity(); entityWithInheritedProperties = new EntityWithInheritedProperties(); basicEntity = new LifecycleAwareEntity(); + // the line below looks ridiculous, but it is necessary to set static field of metadata base URI + new JenaProperties("http://localhost/iri/", new StoreParamsProperties()); setupRequestContext(); } @@ -55,7 +66,7 @@ public void testIriGeneration() { assertNotNull(iri); assertTrue(iri.isURI()); validateIRI(iri.getURI()); - assertTrue(iri.getURI().startsWith(CONFIG.jena.metadataBaseIRI)); + assertTrue(iri.getURI().startsWith("http://localhost/iri/")); dao.write(entity); assertEquals(iri, entity.getIri()); assertNotEquals(iri, dao.write(new Entity()).getIri()); @@ -398,7 +409,7 @@ private static class WithRequired extends PersistentEntity { } @RDFType("http://example.com/iri/NoDefaultConstructor") - private class NoDefaultConstructor extends PersistentEntity { + private static class NoDefaultConstructor extends PersistentEntity { final int value; private NoDefaultConstructor(int value) { diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/RestoreTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/RestoreTest.java index dbe65383a4..c50a90fd66 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/RestoreTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/RestoreTest.java @@ -1,14 +1,15 @@ package io.fairspace.saturn.rdf.transactions; import java.io.File; -import java.io.IOException; import org.apache.jena.rdf.model.Statement; import org.junit.After; import org.junit.Before; import org.junit.Test; -import io.fairspace.saturn.config.Config; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.SaturnDatasetFactory; import static java.util.UUID.randomUUID; @@ -27,19 +28,19 @@ public class RestoreTest { createProperty("http://example.com/property2"), createResource("http://example.com/object2")); - private Config.Jena config; + private JenaProperties config; @Before public void before() { - config = new Config.Jena(); - config.datasetPath = new File(getTempDirectory(), randomUUID().toString()); - config.transactionLogPath = new File(getTempDirectory(), randomUUID().toString()); + config = new JenaProperties("http://localhost/iri/", new StoreParamsProperties()); + config.setDatasetPath(new File(getTempDirectory(), randomUUID().toString())); + config.setTransactionLogPath(new File(getTempDirectory(), randomUUID().toString())); } @After public void after() { - config.transactionLogPath.delete(); - config.datasetPath.delete(); + config.getTransactionLogPath().delete(); + config.getDatasetPath().delete(); } @Test @@ -50,8 +51,8 @@ public void restoreWorksAsExpected() throws Exception { txn1.executeWrite(m -> m.add(stmt2)); } - deleteDirectory(config.datasetPath); - assertFalse(config.datasetPath.exists()); + deleteDirectory(config.getDatasetPath()); + assertFalse(config.getDatasetPath().exists()); try (var txn2 = newDataset()) { txn2.executeRead(m -> { @@ -77,14 +78,15 @@ public void restoreListsWorksAsExpected() throws Exception { txn1.close(); - deleteDirectory(config.datasetPath); + deleteDirectory(config.getDatasetPath()); try (var txn2 = newDataset()) { txn2.executeRead(m -> assertEquals(before, m.listStatements().toSet())); } } - private Transactions newDataset() throws IOException { - return new BulkTransactions(SaturnDatasetFactory.connect(config, null)); + private Transactions newDataset() { + var viewProperties = new ViewsProperties(); + return new BulkTransactions(SaturnDatasetFactory.connect(viewProperties, config, null, null)); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TransactionsTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TransactionsTest.java index 6ea41b414f..841d225411 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TransactionsTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TransactionsTest.java @@ -10,7 +10,9 @@ import org.junit.Before; import org.junit.Test; -import io.fairspace.saturn.config.Config; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.rdf.SaturnDatasetFactory; import static java.util.UUID.randomUUID; @@ -20,21 +22,24 @@ import static org.apache.jena.query.ReadWrite.WRITE; public class TransactionsTest { - private Config.Jena config = new Config.Jena(); + + private final JenaProperties jenaProperties = + new JenaProperties("http://localhost/iri/", new StoreParamsProperties()); private Dataset ds; @Before public void before() { - config.datasetPath = new File(getTempDirectory(), randomUUID().toString()); - config.transactionLogPath = new File(getTempDirectory(), randomUUID().toString()); - - ds = SaturnDatasetFactory.connect(config, null); + jenaProperties.setDatasetPath(new File(getTempDirectory(), randomUUID().toString())); + jenaProperties.setTransactionLogPath( + new File(getTempDirectory(), randomUUID().toString())); + var viewProperties = new ViewsProperties(); + ds = SaturnDatasetFactory.connect(viewProperties, jenaProperties, null, null); } @After public void after() throws IOException { ds.close(); - deleteDirectory(config.datasetPath); + deleteDirectory(jenaProperties.getDatasetPath()); ds = null; } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraphTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraphTest.java index a69d3ac0e0..079bf224be 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraphTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/rdf/transactions/TxnLogDatasetGraphTest.java @@ -5,18 +5,26 @@ import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.rdf.model.Statement; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.security.core.context.SecurityContextHolder; import static io.fairspace.saturn.TestUtils.setupRequestContext; -import static org.apache.jena.rdf.model.ResourceFactory.*; +import static org.apache.jena.rdf.model.ResourceFactory.createPlainLiteral; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createStatement; import static org.apache.jena.sparql.core.DatasetGraphFactory.createTxnMem; import static org.apache.jena.sparql.core.Quad.defaultGraphNodeGenerated; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; @RunWith(MockitoJUnitRunner.class) public class TxnLogDatasetGraphTest { @@ -37,12 +45,17 @@ public void before() { txn = new BulkTransactions(ds); } + @After + public void tearDown() { + SecurityContextHolder.clearContext(); + } + @Test public void shouldLogWriteTransactions() throws IOException { txn.calculateWrite(m -> m.add(statement).remove(statement)); verify(log).onBegin(); - verify(log).onMetadata(eq("userid"), eq("fullname"), anyLong()); + verify(log).onMetadata(eq("user"), eq("fullname"), anyLong()); verify(log) .onAdd( defaultGraphNodeGenerated, @@ -67,7 +80,7 @@ public void shouldHandleAbortedTransactions() throws IOException { }); verify(log).onBegin(); - verify(log).onMetadata(eq("userid"), eq("fullname"), anyLong()); + verify(log).onMetadata(eq("user"), eq("fullname"), anyLong()); verify(log) .onAdd( defaultGraphNodeGenerated, @@ -85,7 +98,7 @@ public void shouldHandleAbortedTransactions() throws IOException { } @Test - public void shouldNotLogReadTransactions() throws IOException { + public void shouldNotLogReadTransactions() { txn.executeRead(m -> m.listStatements().toList()); verifyNoMoreInteractions(log); @@ -101,7 +114,7 @@ public void testThatAnExceptionWithinATransactionIsHandledProperly() throws IOEx } catch (Exception ignore) { } verify(log).onBegin(); - verify(log).onMetadata(eq("userid"), eq("fullname"), anyLong()); + verify(log).onMetadata(eq("user"), eq("fullname"), anyLong()); verify(log) .onAdd( defaultGraphNodeGenerated, diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java deleted file mode 100644 index dcaaf4ee22..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/errors/ErrorHelperTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.fairspace.saturn.services.errors; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.Test; - -import io.fairspace.saturn.vocabulary.FS; - -import static org.junit.Assert.assertEquals; - -public class ErrorHelperTest { - - @Test - public void errorBody() throws IOException { - var errorBody = ErrorHelper.errorBody(100, "BaseEvent", List.of("a", "b")); - - // Parse the json body - Map parsedMap = new ObjectMapper().readValue(errorBody, Map.class); - - // Expect the properties to be serialized as json - assertEquals(100, parsedMap.get("status")); - assertEquals("BaseEvent", parsedMap.get("message")); - assertEquals(List.of("a", "b"), parsedMap.get("details")); - } - - @Test - public void errorBodyContext() throws IOException { - var errorBody = ErrorHelper.errorBody(100, "BaseEvent", List.of("a", "b")); - - // Parse the json body - Map parsedMap = new ObjectMapper().readValue(errorBody, Map.class); - - // Expect the properties to be serialized as json - assertEquals(FS.ERROR_URI, parsedMap.get("@type")); - assertEquals( - Map.of( - "details", FS.ERROR_DETAILS_URI, - "message", FS.ERROR_MESSAGE_URI, - "status", FS.ERROR_STATUS_URI), - parsedMap.get("@context")); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/health/HealthServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/health/HealthServiceTest.java deleted file mode 100644 index 2c9f60453d..0000000000 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/health/HealthServiceTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.fairspace.saturn.services.health; - -import java.io.IOException; -import java.sql.SQLException; - -import com.zaxxer.hikari.HikariDataSource; -import io.milton.http.exceptions.BadRequestException; -import io.milton.http.exceptions.ConflictException; -import io.milton.http.exceptions.NotAuthorizedException; -import lombok.SneakyThrows; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -import io.fairspace.saturn.PostgresAwareTest; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ViewsConfig; -import io.fairspace.saturn.services.views.ViewStoreClientFactory; - -import static io.fairspace.saturn.TestUtils.loadViewsConfig; - -@RunWith(MockitoJUnitRunner.class) -public class HealthServiceTest extends PostgresAwareTest { - HealthService healthService; - ViewStoreClientFactory viewStoreClientFactory; - - @Before - public void before() - throws SQLException, NotAuthorizedException, BadRequestException, ConflictException, IOException { - var viewDatabase = new Config.ViewDatabase(); - viewDatabase.url = postgres.getJdbcUrl(); - viewDatabase.username = postgres.getUsername(); - viewDatabase.password = postgres.getPassword(); - viewDatabase.maxPoolSize = 5; - ViewsConfig config = loadViewsConfig("src/test/resources/test-views.yaml"); - viewStoreClientFactory = new ViewStoreClientFactory(config, viewDatabase, new Config.Search()); - - healthService = new HealthService(viewStoreClientFactory.dataSource); - } - - @Test - public void testRetrieveStatus_UP() { - healthService = new HealthService(null); - var health = healthService.getHealth(); - - Assert.assertEquals(health.getStatus(), HealthStatus.UP); - Assert.assertTrue(health.getComponents().isEmpty()); - } - - @Test - public void testRetrieveStatusWithViewDatabase_UP() { - var health = healthService.getHealth(); - - Assert.assertEquals(health.getStatus(), HealthStatus.UP); - Assert.assertEquals(health.getComponents().size(), 1); - Assert.assertEquals(health.getComponents().get("viewDatabase"), HealthStatus.UP); - } - - @SneakyThrows - @Test - public void testRetrieveStatusWithViewDatabase_DOWN() { - ((HikariDataSource) viewStoreClientFactory.dataSource).close(); - var health = healthService.getHealth(); - - Assert.assertEquals(health.getStatus(), HealthStatus.DOWN); - Assert.assertEquals(health.getComponents().size(), 1); - Assert.assertEquals(health.getComponents().get("viewDatabase"), HealthStatus.DOWN); - } -} diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java index 9d0aec3c11..b67f6a8ed5 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/maintenance/MaintenanceServiceTest.java @@ -5,6 +5,7 @@ import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.ViewsProperties; import io.fairspace.saturn.services.AccessDeniedException; import io.fairspace.saturn.services.ConflictException; import io.fairspace.saturn.services.NotAvailableException; @@ -13,6 +14,7 @@ import io.fairspace.saturn.services.views.ViewService; import io.fairspace.saturn.services.views.ViewStoreClientFactory; +import static io.fairspace.saturn.TestUtils.loadViewsConfig; import static io.fairspace.saturn.services.maintenance.MaintenanceService.MAINTENANCE_IS_IN_PROGRESS; import static io.fairspace.saturn.services.maintenance.MaintenanceService.SERVICE_NOT_AVAILABLE; @@ -31,8 +33,9 @@ public class MaintenanceServiceTest { private final Dataset dataset = mock(Dataset.class); private final ViewStoreClientFactory viewStoreClientFactory = mock(ViewStoreClientFactory.class); private final ViewService viewService = mock(ViewService.class); - private final MaintenanceService sut = - spy(new MaintenanceService(userService, dataset, viewStoreClientFactory, viewService)); + private final ViewsProperties viewsProperties = loadViewsConfig("src/test/resources/test-views.yaml"); + private final MaintenanceService sut = spy(new MaintenanceService( + viewsProperties, userService, dataset, viewStoreClientFactory, viewService, "localhost")); @Test public void testReindexingIsNotAllowedForNotAdmins() { diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceTest.java index f2a2377cf3..e9ae4a0fe4 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceTest.java @@ -1,5 +1,7 @@ package io.fairspace.saturn.services.metadata; +import java.util.List; + import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Property; @@ -54,7 +56,7 @@ public class MetadataServiceTest { private static final Statement STMT1 = createStatement(S1, P1, S2); private static final Statement STMT2 = createStatement(S2, P1, S3); - private Dataset ds = createTxnMem(); + private final Dataset ds = createTxnMem(); @Spy private Transactions txn = new SimpleTransactions(ds); @@ -74,8 +76,14 @@ public void setUp() { Dataset ds = wrap(dsg); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); - - api = new MetadataService(txn, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + var systemVocabulary = model.read("system-vocabulary.ttl"); + + api = new MetadataService( + txn, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); } @Test @@ -124,7 +132,6 @@ public void testPutWillNotRemoveExistingStatements() { // Now ensure that the existing triples are still there // and the new ones are added txn.executeRead(model -> { - ; assertTrue(model.contains(EXISTING1)); assertTrue(model.contains(EXISTING2)); assertTrue(model.contains(STMT1)); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceValidationTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceValidationTest.java index 8ba41a15f2..88d7a081f4 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceValidationTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/MetadataServiceValidationTest.java @@ -73,7 +73,8 @@ public void setUp() { Dataset ds = wrap(dsg); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); - api = new MetadataService(txn, vocabulary, validator, permissions); + var systemVocabulary = model.read("system-vocabulary.ttl"); + api = new MetadataService(txn, vocabulary, systemVocabulary, validator, permissions); setupRequestContext(); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/ReadableMetadataServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/ReadableMetadataServiceTest.java index adc9c5e2cc..a08a8c48cd 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/ReadableMetadataServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/ReadableMetadataServiceTest.java @@ -17,12 +17,17 @@ import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.vocabulary.FS; -import static io.fairspace.saturn.vocabulary.Vocabularies.SYSTEM_VOCABULARY; - import static org.apache.jena.query.DatasetFactory.createTxnMem; import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; -import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.junit.Assert.*; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createStatement; +import static org.apache.jena.rdf.model.ResourceFactory.createStringLiteral; +import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; +import static org.apache.jena.riot.RDFDataMgr.loadModel; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -39,9 +44,10 @@ public class ReadableMetadataServiceTest { private static final Statement LBL_STMT1 = createStatement(S1, RDFS.label, createStringLiteral("subject1")); private static final Statement LBL_STMT2 = createStatement(S2, RDFS.label, createStringLiteral("subject2")); - private Transactions txn = new SimpleTransactions(createTxnMem()); + private final Transactions txn = new SimpleTransactions(createTxnMem()); private MetadataService api; - private Model vocabulary = SYSTEM_VOCABULARY.union(createDefaultModel()); + private final Model systemVocabulary = loadModel("system-vocabulary.ttl"); + private final Model vocabulary = systemVocabulary.union(createDefaultModel()); @Mock MetadataPermissions permissions; @@ -49,7 +55,7 @@ public class ReadableMetadataServiceTest { @Before public void setUp() { when(permissions.canReadMetadata(any())).thenReturn(true); - api = new MetadataService(txn, vocabulary, null, permissions); + api = new MetadataService(txn, vocabulary, systemVocabulary, null, permissions); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidatorTest.java index a0de4392fd..76b314b173 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidatorTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ProtectMachineOnlyPredicatesValidatorTest.java @@ -12,10 +12,13 @@ import static io.fairspace.saturn.rdf.ModelUtils.EMPTY_MODEL; import static io.fairspace.saturn.rdf.ModelUtils.modelOf; -import static io.fairspace.saturn.vocabulary.Vocabularies.SYSTEM_VOCABULARY; -import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.mockito.Mockito.*; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createStatement; +import static org.apache.jena.riot.RDFDataMgr.loadModel; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; @RunWith(MockitoJUnitRunner.class) public class ProtectMachineOnlyPredicatesValidatorTest { @@ -27,7 +30,7 @@ public class ProtectMachineOnlyPredicatesValidatorTest { private static final Property P2 = createProperty("https://fairspace.nl/ontology/P2"); private final ProtectMachineOnlyPredicatesValidator validator = - new ProtectMachineOnlyPredicatesValidator(SYSTEM_VOCABULARY); + new ProtectMachineOnlyPredicatesValidator(loadModel("system-vocabulary.ttl")); @Mock private ViolationHandler violationHandler; diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java index 32d7c9f75a..93dbf09b25 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/ShaclValidatorTest.java @@ -16,13 +16,21 @@ import io.fairspace.saturn.vocabulary.FS; -import static io.fairspace.saturn.rdf.ModelUtils.*; -import static io.fairspace.saturn.vocabulary.Vocabularies.SYSTEM_VOCABULARY; +import static io.fairspace.saturn.rdf.ModelUtils.EMPTY_MODEL; +import static io.fairspace.saturn.rdf.ModelUtils.asNode; +import static io.fairspace.saturn.rdf.ModelUtils.modelOf; import static org.apache.jena.rdf.model.ModelFactory.createDefaultModel; -import static org.apache.jena.rdf.model.ResourceFactory.*; -import static org.eclipse.jetty.util.ProcessorUtils.availableProcessors; -import static org.mockito.Mockito.*; +import static org.apache.jena.rdf.model.ResourceFactory.createProperty; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.apache.jena.rdf.model.ResourceFactory.createStringLiteral; +import static org.apache.jena.rdf.model.ResourceFactory.createTypedLiteral; +import static org.apache.jena.riot.RDFDataMgr.loadModel; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; @RunWith(MockitoJUnitRunner.class) public class ShaclValidatorTest { @@ -32,17 +40,17 @@ public class ShaclValidatorTest { private static final Resource closedClassShape = createResource("http://example.com/ClosedClassShape"); private ShaclValidator validator; - private Model vocabulary; @Mock private ViolationHandler violationHandler; @Before public void setUp() { - vocabulary = SYSTEM_VOCABULARY.union(createDefaultModel() - .add(closedClassShape, RDF.type, SHACLM.NodeShape) - .add(closedClassShape, SHACLM.targetClass, closedClass) - .add(closedClassShape, SHACLM.closed, createTypedLiteral(true))); + Model vocabulary = loadModel("system-vocabulary.ttl") + .union(createDefaultModel() + .add(closedClassShape, RDF.type, SHACLM.NodeShape) + .add(closedClassShape, SHACLM.targetClass, closedClass) + .add(closedClassShape, SHACLM.closed, createTypedLiteral(true))); validator = new ShaclValidator(vocabulary); } @@ -211,7 +219,7 @@ public void validationForSomethingReferringToABlankNode2() { @Test public void multipleResourcesAreValidatedAsExpected() { var model = createDefaultModel(); - for (int i = 0; i < 2 * availableProcessors(); i++) { + for (int i = 0; i < 2 * Runtime.getRuntime().availableProcessors(); i++) { var resource = createResource(); model.add(resource, RDF.type, FS.File).add(resource, FS.createdBy, createTypedLiteral(123)); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidatorTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidatorTest.java index 65a8036f65..09e54dd53d 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidatorTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/metadata/validation/URIPrefixValidatorTest.java @@ -26,7 +26,7 @@ public class URIPrefixValidatorTest { @Before public void setUp() { - validator = new URIPrefixValidator("http://example.com/api/webdav"); + validator = new URIPrefixValidator("http://example.com"); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java index deb6328971..c3dfebcb9d 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/JdbcFileSearchServiceTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.sql.SQLException; +import java.util.List; import io.milton.http.ResourceFactory; import io.milton.http.exceptions.BadRequestException; @@ -13,7 +14,6 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.core.DatasetGraphFactory; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -22,9 +22,10 @@ import org.mockito.junit.MockitoJUnitRunner; import io.fairspace.saturn.PostgresAwareTest; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ConfigLoader; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.config.properties.CacheProperties; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -36,7 +37,11 @@ import io.fairspace.saturn.services.metadata.validation.UniqueLabelValidator; import io.fairspace.saturn.services.users.User; import io.fairspace.saturn.services.users.UserService; -import io.fairspace.saturn.services.views.*; +import io.fairspace.saturn.services.views.MaterializedViewService; +import io.fairspace.saturn.services.views.ViewService; +import io.fairspace.saturn.services.views.ViewStoreClient; +import io.fairspace.saturn.services.views.ViewStoreClientFactory; +import io.fairspace.saturn.services.views.ViewStoreReader; import io.fairspace.saturn.services.workspaces.Workspace; import io.fairspace.saturn.services.workspaces.WorkspaceRole; import io.fairspace.saturn.services.workspaces.WorkspaceService; @@ -44,16 +49,23 @@ import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.loadViewsConfig; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static org.apache.jena.query.DatasetFactory.wrap; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class JdbcFileSearchServiceTest extends PostgresAwareTest { static final String BASE_PATH = "/api/webdav"; - static final String baseUri = ConfigLoader.CONFIG.publicUrl + BASE_PATH; + static final String baseUri = "http://localhost:8080" + BASE_PATH; @Mock BlobStore store; @@ -64,79 +76,103 @@ public class JdbcFileSearchServiceTest extends PostgresAwareTest { @Mock private MetadataPermissions permissions; + @Mock + private MaterializedViewService materializedViewService; + WorkspaceService workspaceService; MetadataService api; FileSearchService fileSearchService; MaintenanceService maintenanceService; + private DAO dao; + User user; - Authentication.User userAuthentication; + User user2; User workspaceManager; - Authentication.User workspaceManagerAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); + mockAuthentication(USER); lenient().when(userService.currentUser()).thenReturn(user); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } + private void setupUsers() { + user = createTestUser("user", false); + user.setCanViewPublicMetadata(true); + dao.write(user); + user2 = createTestUser("user2", false); + dao.write(user2); + workspaceManager = createTestUser("workspace-admin", false); + dao.write(workspaceManager); + admin = createTestUser("admin", true); + dao.write(admin); + } + @Before public void before() throws SQLException, NotAuthorizedException, BadRequestException, ConflictException, IOException { - var viewDatabase = new Config.ViewDatabase(); - viewDatabase.url = postgres.getJdbcUrl(); - viewDatabase.username = postgres.getUsername(); - viewDatabase.password = postgres.getPassword(); - viewDatabase.maxPoolSize = 5; - ViewsConfig config = loadViewsConfig("src/test/resources/test-views.yaml"); - var viewStoreClientFactory = new ViewStoreClientFactory(config, viewDatabase, new Config.Search()); - - var dsg = new TxnIndexDatasetGraph(DatasetGraphFactory.createTxnMem(), viewStoreClientFactory); + JenaProperties.setMetadataBaseIRI("http://localhost/iri/"); + var viewsProperties = loadViewsConfig("src/test/resources/test-views.yaml"); + var searchProperties = buildSearchProperties(); + var viewDatabase = buildViewDatabaseConfig(); + var configuration = new ViewStoreClient.ViewStoreConfiguration(viewsProperties); + var dataSource = getDataSource(viewDatabase); + var viewStoreClientFactory = new ViewStoreClientFactory( + viewsProperties, viewDatabase, materializedViewService, dataSource, configuration); + var dsg = new TxnIndexDatasetGraph( + viewsProperties, DatasetGraphFactory.createTxnMem(), viewStoreClientFactory, "http://localhost:8080"); Dataset ds = wrap(dsg); Transactions tx = new SimpleTransactions(ds); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + var systemVocabulary = model.read("system-vocabulary.ttl"); + var viewStoreReader = + new ViewStoreReader(searchProperties, viewsProperties, viewStoreClientFactory, configuration); + var viewService = new ViewService( + searchProperties, + new CacheProperties(), + viewsProperties, + ds, + viewStoreReader, + viewStoreClientFactory, + permissions); - var viewService = new ViewService(ConfigLoader.CONFIG, config, ds, viewStoreClientFactory, permissions); - - maintenanceService = new MaintenanceService(userService, ds, viewStoreClientFactory, viewService); + maintenanceService = new MaintenanceService( + viewsProperties, userService, ds, viewStoreClientFactory, viewService, "http://localhost:8080"); workspaceService = new WorkspaceService(tx, userService); var context = new Context(); - - var davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); - - fileSearchService = new JdbcFileSearchService( - ConfigLoader.CONFIG.search, - loadViewsConfig("src/test/resources/test-views.yaml"), - viewStoreClientFactory, - tx, - davFactory.root); + var davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); + fileSearchService = new JdbcFileSearchService(tx, davFactory.root, viewStoreReader); when(permissions.canWriteMetadata(any())).thenReturn(true); + api = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); - api = new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + dao = new DAO(model); - userAuthentication = mockAuthentication("user"); - user = createTestUser("user", false); - new DAO(model).write(user); - workspaceManager = createTestUser("workspace-admin", false); - new DAO(model).write(workspaceManager); - workspaceManagerAuthentication = mockAuthentication("workspace-admin"); - adminAuthentication = mockAuthentication("admin"); - admin = createTestUser("admin", true); - new DAO(model).write(admin); + setupUsers(); setupRequestContext(); - request = getCurrentRequest(); + var request = getCurrentRequest(); selectAdmin(); @@ -176,8 +212,8 @@ public void testSearchFiles() { var results = fileSearchService.searchFiles(request); Assert.assertEquals(2, results.size()); // Expect the results to be sorted by id - Assert.assertEquals("sample-s2-b-rna.fastq", results.get(0).getLabel()); - Assert.assertEquals("sample-s2-b-rna_copy.fastq", results.get(1).getLabel()); + Assert.assertEquals("sample-s2-b-rna.fastq", results.get(0).label()); + Assert.assertEquals("sample-s2-b-rna_copy.fastq", results.get(1).label()); } @Test @@ -191,7 +227,7 @@ public void testSearchFilesRestrictsToAccessibleCollections() { selectAdmin(); results = fileSearchService.searchFiles(request); Assert.assertEquals(1, results.size()); - Assert.assertEquals("coffee.jpg", results.get(0).getLabel()); + Assert.assertEquals("coffee.jpg", results.getFirst().label()); } @Test @@ -206,7 +242,7 @@ public void testSearchFilesRestrictsToAccessibleCollectionsAfterReindexing() { selectAdmin(); results = fileSearchService.searchFiles(request); Assert.assertEquals(1, results.size()); - Assert.assertEquals("coffee.jpg", results.get(0).getLabel()); + Assert.assertEquals("coffee.jpg", results.getFirst().label()); } @Test @@ -216,11 +252,11 @@ public void testSearchFilesRestrictsToParentDirectory() { // There is one file named coffee.jpg in coll1. request.setQuery("coffee"); - request.setParentIRI(ConfigLoader.CONFIG.publicUrl + "/api/webdav/coll1"); + request.setParentIRI("http://localhost:8080/api/webdav/coll1"); var results = fileSearchService.searchFiles(request); Assert.assertEquals(1, results.size()); - request.setParentIRI(ConfigLoader.CONFIG.publicUrl + "/api/webdav/coll2"); + request.setParentIRI("http://localhost:8080/api/webdav/coll2"); results = fileSearchService.searchFiles(request); Assert.assertEquals(0, results.size()); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java index a561e3ad5a..c1595df07f 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/search/SparqlFileSearchServiceTest.java @@ -1,6 +1,7 @@ package io.fairspace.saturn.services.search; import java.io.IOException; +import java.util.List; import io.milton.http.ResourceFactory; import io.milton.http.exceptions.BadRequestException; @@ -13,14 +14,14 @@ import org.apache.jena.sparql.core.DatasetGraphFactory; import org.apache.jena.sparql.core.DatasetImpl; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import io.fairspace.saturn.config.ConfigLoader; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.FileSearchRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; @@ -38,17 +39,22 @@ import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static org.apache.jena.query.DatasetFactory.wrap; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class SparqlFileSearchServiceTest { static final String BASE_PATH = "/api/webdav"; - static final String baseUri = ConfigLoader.CONFIG.publicUrl + BASE_PATH; + static final String baseUri = "http://localhost:8080" + BASE_PATH; @Mock BlobStore store; @@ -64,37 +70,26 @@ public class SparqlFileSearchServiceTest { FileSearchService fileSearchService; User user; - Authentication.User userAuthentication; User user2; - Authentication.User user2Authentication; User workspaceManager; - Authentication.User workspaceManagerAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); lenient().when(userService.currentUser()).thenReturn(user); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); lenient().when(userService.currentUser()).thenReturn(admin); } private void setupUsers(Model model) { - userAuthentication = mockAuthentication("user"); user = createTestUser("user", false); user.setCanViewPublicMetadata(true); new DAO(model).write(user); - user2Authentication = mockAuthentication("user2"); user2 = createTestUser("user2", false); new DAO(model).write(user2); workspaceManager = createTestUser("workspace-admin", false); new DAO(model).write(workspaceManager); - workspaceManagerAuthentication = mockAuthentication("workspace-admin"); - adminAuthentication = mockAuthentication("admin"); admin = createTestUser("admin", true); new DAO(model).write(admin); } @@ -106,11 +101,21 @@ public void before() throws NotAuthorizedException, BadRequestException, Conflic Transactions tx = new SimpleTransactions(ds); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); + var systemVocabulary = model.read("system-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); workspaceService = new WorkspaceService(tx, userService); var context = new Context(); - var davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); + + var davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); var metadataPermissions = new MetadataPermissions(workspaceService, davFactory, userService); var filteredDatasetGraph = new FilteredDatasetGraph(ds.asDatasetGraph(), metadataPermissions); var filteredDataset = DatasetImpl.wrap(filteredDatasetGraph); @@ -118,12 +123,17 @@ public void before() throws NotAuthorizedException, BadRequestException, Conflic fileSearchService = new SparqlFileSearchService(filteredDataset); when(permissions.canWriteMetadata(any())).thenReturn(true); - api = new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + api = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); setupUsers(model); setupRequestContext(); - request = getCurrentRequest(); + var request = getCurrentRequest(); selectAdmin(); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/users/UserServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/users/UserServiceTest.java index d7804d204d..bcafb79af0 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/users/UserServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/users/UserServiceTest.java @@ -1,34 +1,39 @@ package io.fairspace.saturn.services.users; -import java.util.*; -import java.util.stream.*; - -import org.eclipse.jetty.server.*; -import org.junit.*; -import org.junit.runner.*; -import org.keycloak.admin.client.resource.*; -import org.keycloak.representations.idm.*; -import org.mockito.*; -import org.mockito.junit.*; - -import io.fairspace.saturn.config.*; -import io.fairspace.saturn.rdf.dao.*; -import io.fairspace.saturn.rdf.transactions.*; -import io.fairspace.saturn.services.workspaces.*; - -import static io.fairspace.saturn.TestUtils.*; -import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; -import static io.fairspace.saturn.rdf.SparqlUtils.generateMetadataIri; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import io.fairspace.saturn.config.properties.KeycloakClientProperties; +import io.fairspace.saturn.rdf.dao.DAO; +import io.fairspace.saturn.rdf.transactions.SimpleTransactions; +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceService; + +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; +import static io.fairspace.saturn.rdf.SparqlUtils.generateMetadataIriFromId; import static org.apache.jena.query.DatasetFactory.createTxnMem; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class UserServiceTest { - private org.eclipse.jetty.server.Request request; - private Transactions tx = new SimpleTransactions(createTxnMem()); + private final Transactions tx = new SimpleTransactions(createTxnMem()); private WorkspaceService workspaceService; @Mock @@ -36,36 +41,26 @@ public class UserServiceTest { private UserService userService; User user; - Authentication.User userAuthentication; User admin; - Authentication.User adminAuthentication; List keycloakUsers; - private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); - } - - private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); - } - @Before public void setUp() { - setupRequestContext(); - request = getCurrentRequest(); + setupRequestContext(ADMIN); tx.executeWrite(model -> { - userAuthentication = mockAuthentication("user"); - user = createTestUser("user", false); + mockAuthentication(USER); + user = createTestUser(USER, false); user.setCanViewPublicData(true); user.setCanViewPublicMetadata(true); - new DAO(model).write(user); - adminAuthentication = mockAuthentication("admin"); - admin = createTestUser("admin", true); - new DAO(model).write(admin); + DAO dao = new DAO(model); + dao.write(user); + mockAuthentication(ADMIN); + admin = createTestUser(ADMIN, true); + dao.write(admin); }); - keycloakUsers = List.of(user, admin).stream() + keycloakUsers = Stream.of(user, admin) .map(user -> { var keycloakUser = new UserRepresentation(); keycloakUser.setId(user.getId()); @@ -75,7 +70,7 @@ public void setUp() { .collect(Collectors.toList()); when(usersResource.list(any(), any())).thenReturn(keycloakUsers); - userService = new UserService(ConfigLoader.CONFIG.auth, tx, usersResource); + userService = new UserService(new KeycloakClientProperties(), tx, usersResource); workspaceService = new WorkspaceService(tx, userService); selectAdmin(); @@ -83,6 +78,14 @@ public void setUp() { workspaceService.createWorkspace(Workspace.builder().code("ws1").build()); } + public static void selectRegularUser() { + mockAuthentication(USER); + } + + public static void selectAdmin() { + mockAuthentication(ADMIN); + } + private void triggerKeycloakUserUpdate() { selectAdmin(); var update = new UserRolesUpdate(); @@ -92,7 +95,7 @@ private void triggerKeycloakUserUpdate() { // Change Keycloak info, triggering a database write // when the user cache is refreshed - keycloakUsers.get(0).setLastName("Updated"); + keycloakUsers.getFirst().setLastName("Updated"); when(usersResource.list(any(), any())).thenReturn(keycloakUsers); } @@ -100,19 +103,20 @@ private void triggerKeycloakUserUpdate() { * In some parts of the application, the current user object is requested * to perform access checks. Requesting the current user may trigger a read from * Keycloak, as the list of users is cached by Saturn only for limited time (see - * {@link UserService#UserService(Config.Auth, Transactions, UsersResource)}). - * + * {@link UserService(KeycloakClientProperties, Transactions, UsersResource)}). + *

* While fetching the list of users, Saturn may update the user objects in the RDF database * when some user properties have changed in Keycloak or when new users have been added. * That update did trigger a transaction error when occurring during a read action, such as fetching * the list of workspaces (see VRE-1455). - * + *

* This test ensures that such updates happen asynchronously, not interfering with the * ongoing read transaction. */ @Test public void testFetchUsersWhileFetchingWorkspaces() throws InterruptedException { - var pristineUser = tx.calculateRead(model -> new DAO(model).read(User.class, generateMetadataIri("user"))); + var pristineUser = + tx.calculateRead(model -> new DAO(model).read(User.class, generateMetadataIriFromId("user"))); Assert.assertEquals("user", pristineUser.getName()); triggerKeycloakUserUpdate(); @@ -123,7 +127,7 @@ public void testFetchUsersWhileFetchingWorkspaces() throws InterruptedException workspaceService.listWorkspaces(); Thread.sleep(500); - var updatedUser = tx.calculateRead(model -> new DAO(model).read(User.class, generateMetadataIri("user"))); + var updatedUser = tx.calculateRead(model -> new DAO(model).read(User.class, generateMetadataIriFromId("user"))); // Check that the updated user was correctly saved to the database. Assert.assertEquals("Updated", updatedUser.getName()); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java index d2d9629b7e..4ab572640d 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/JdbcQueryServiceTest.java @@ -3,8 +3,10 @@ import java.io.IOException; import java.sql.SQLException; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import javax.sql.DataSource; import io.milton.http.ResourceFactory; import io.milton.http.exceptions.BadRequestException; @@ -16,7 +18,6 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.core.DatasetGraphFactory; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -25,9 +26,14 @@ import org.mockito.junit.MockitoJUnitRunner; import io.fairspace.saturn.PostgresAwareTest; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ConfigLoader; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.config.properties.CacheProperties; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.ValueDto; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -46,6 +52,8 @@ import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; import static io.fairspace.saturn.TestUtils.createTestUser; import static io.fairspace.saturn.TestUtils.loadViewsConfig; import static io.fairspace.saturn.TestUtils.mockAuthentication; @@ -61,7 +69,8 @@ @RunWith(MockitoJUnitRunner.class) public class JdbcQueryServiceTest extends PostgresAwareTest { static final String BASE_PATH = "/api/webdav"; - static final String baseUri = ConfigLoader.CONFIG.publicUrl + BASE_PATH; + static final String PUBLIC_URL = "http://localhost:8080"; + static final String baseUri = PUBLIC_URL + BASE_PATH; static final String SAMPLE_NATURE_BLOOD = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#C12434"; static final String ANALYSIS_TYPE_RNA_SEQ = "https://institut-curie.org/osiris#O6-12"; static final String ANALYSIS_TYPE_IMAGING = "https://institut-curie.org/osiris#C37-2"; @@ -80,78 +89,96 @@ public class JdbcQueryServiceTest extends PostgresAwareTest { QueryService sut; MaintenanceService maintenanceService; + private DAO dao; + User user; - Authentication.User userAuthentication; User workspaceManager; - Authentication.User workspaceManagerAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); + mockAuthentication(USER); lenient().when(userService.currentUser()).thenReturn(user); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } @Before public void before() throws SQLException, NotAuthorizedException, BadRequestException, ConflictException, IOException { - var viewDatabase = new Config.ViewDatabase(); - viewDatabase.url = postgres.getJdbcUrl(); - viewDatabase.username = postgres.getUsername(); - viewDatabase.password = postgres.getPassword(); - viewDatabase.maxPoolSize = 5; - ViewsConfig config = loadViewsConfig("src/test/resources/test-views.yaml"); - var viewStoreClientFactory = new ViewStoreClientFactory(config, viewDatabase, new Config.Search()); - - var dsg = new TxnIndexDatasetGraph(DatasetGraphFactory.createTxnMem(), viewStoreClientFactory); + JenaProperties.setMetadataBaseIRI("http://localhost/iri/"); + var viewDatabase = buildViewDatabaseConfig(); + ViewsProperties viewsProperties = loadViewsConfig("src/test/resources/test-views.yaml"); + SearchProperties searchProperties = new SearchProperties(); + searchProperties.setCountRequestTimeout(60000); + searchProperties.setPageRequestTimeout(10000); + searchProperties.setMaxJoinItems(50); + var configuration = new ViewStoreClient.ViewStoreConfiguration(viewsProperties); + DataSource dataSource = getDataSource(viewDatabase); + MaterializedViewService materializedViewService = new MaterializedViewService( + dataSource, configuration, viewsProperties, searchProperties.getMaxJoinItems()); + var viewStoreClientFactory = new ViewStoreClientFactory( + viewsProperties, viewDatabase, materializedViewService, dataSource, configuration); + + var dsg = new TxnIndexDatasetGraph( + viewsProperties, DatasetGraphFactory.createTxnMem(), viewStoreClientFactory, PUBLIC_URL); Dataset ds = wrap(dsg); Transactions tx = new SimpleTransactions(ds); Model model = ds.getDefaultModel(); - var vocabulary = model.read("test-vocabulary.ttl"); - - var viewService = new ViewService(ConfigLoader.CONFIG, config, ds, viewStoreClientFactory, permissions); + var vocabulary = model.read("test-vocabulary.ttl", "TTL"); + var systemVocabulary = model.read("system-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + + dao = new DAO(model); + + var viewStoreReader = + new ViewStoreReader(searchProperties, viewsProperties, viewStoreClientFactory, configuration); + var viewService = new ViewService( + searchProperties, + new CacheProperties(), + viewsProperties, + ds, + viewStoreReader, + viewStoreClientFactory, + permissions); - maintenanceService = new MaintenanceService(userService, ds, viewStoreClientFactory, viewService); + maintenanceService = new MaintenanceService( + viewsProperties, userService, ds, viewStoreClientFactory, viewService, PUBLIC_URL); workspaceService = new WorkspaceService(tx, userService); var context = new Context(); - var davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); + var davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); - sut = new JdbcQueryService( - ConfigLoader.CONFIG.search, - loadViewsConfig("src/test/resources/test-views.yaml"), - viewStoreClientFactory, - tx, - davFactory.root); + sut = new JdbcQueryService(tx, davFactory.root, viewStoreReader); when(permissions.canWriteMetadata(any())).thenReturn(true); - api = new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + api = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); - userAuthentication = mockAuthentication("user"); - user = createTestUser("user", false); - new DAO(model).write(user); - workspaceManager = createTestUser("workspace-admin", false); - new DAO(model).write(workspaceManager); - workspaceManagerAuthentication = mockAuthentication("workspace-admin"); - adminAuthentication = mockAuthentication("admin"); - admin = createTestUser("admin", true); - new DAO(model).write(admin); + setupUsers(); setupRequestContext(); - request = getCurrentRequest(); + var request = getCurrentRequest(); selectAdmin(); - var taxonomies = model.read("test-taxonomies.ttl"); + var taxonomies = model.read("test-taxonomies.ttl", "TTL"); api.put(taxonomies, Boolean.TRUE); var workspace = workspaceService.createWorkspace( @@ -175,10 +202,20 @@ public void before() coll3.createNew("sample-s2-b-rna_copy.fastq", null, 0L, "chemical/seq-na-fastq"); - var testdata = model.read("testdata.ttl"); + var testdata = model.read("testdata.ttl", "TTL"); + model.write(System.out, "TURTLE"); api.put(testdata, Boolean.TRUE); } + private void setupUsers() { + user = createTestUser("user", false); + dao.write(user); + workspaceManager = createTestUser("workspace-admin", false); + dao.write(workspaceManager); + admin = createTestUser("admin", true); + dao.write(admin); + } + @Test public void testRetrieveSamplePage() { var request = new ViewRequest(); @@ -187,7 +224,7 @@ public void testRetrieveSamplePage() { request.setSize(10); var page = sut.retrieveViewPage(request); Assert.assertEquals(2, page.getRows().size()); - var row = page.getRows().get(0); + var row = page.getRows().getFirst(); Assert.assertEquals( Set.of( "Sample", @@ -199,19 +236,19 @@ public void testRetrieveSamplePage() { row.keySet()); Assert.assertEquals( "Sample A for subject 1", - row.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Blood", - row.get("Sample_nature").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_nature").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Liver", - row.get("Sample_topography").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_topography").stream().findFirst().orElseThrow().label()); Assert.assertEquals( 45.2f, ((Number) row.get("Sample_tumorCellularity").stream() .findFirst() .orElseThrow() - .getValue()) + .value()) .floatValue(), 0.01); } @@ -225,7 +262,7 @@ public void testRetrieveSamplePageAfterReindexing() { request.setSize(10); var page = sut.retrieveViewPage(request); Assert.assertEquals(2, page.getRows().size()); - var row = page.getRows().get(0); + var row = page.getRows().getFirst(); Assert.assertEquals( Set.of( "Sample", @@ -237,19 +274,19 @@ public void testRetrieveSamplePageAfterReindexing() { row.keySet()); Assert.assertEquals( "Sample A for subject 1", - row.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Blood", - row.get("Sample_nature").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_nature").stream().findFirst().orElseThrow().label()); Assert.assertEquals( "Liver", - row.get("Sample_topography").stream().findFirst().orElseThrow().getLabel()); + row.get("Sample_topography").stream().findFirst().orElseThrow().label()); Assert.assertEquals( 45.2f, ((Number) row.get("Sample_tumorCellularity").stream() .findFirst() .orElseThrow() - .getValue()) + .value()) .floatValue(), 0.01); } @@ -305,20 +342,18 @@ public void testRetrieveSamplePageIncludeJoin() { request.setIncludeJoinedViews(true); var page = sut.retrieveViewPage(request); Assert.assertEquals(2, page.getRows().size()); - var row1 = page.getRows().get(0); + var row1 = page.getRows().getFirst(); Assert.assertEquals( "Sample A for subject 1", - row1.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row1.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals(1, row1.get("Subject").size()); var row2 = page.getRows().get(1); Assert.assertEquals( "Sample B for subject 2", - row2.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row2.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( Set.of("RNA-seq", "Whole genome sequencing"), - row2.get("Resource_analysisType").stream() - .map(ValueDTO::getLabel) - .collect(Collectors.toSet())); + row2.get("Resource_analysisType").stream().map(ValueDto::label).collect(Collectors.toSet())); } @Test @@ -331,20 +366,18 @@ public void testRetrieveSamplePageIncludeJoinAfterReindexing() { request.setIncludeJoinedViews(true); var page = sut.retrieveViewPage(request); Assert.assertEquals(2, page.getRows().size()); - var row1 = page.getRows().get(0); + var row1 = page.getRows().getFirst(); Assert.assertEquals( "Sample A for subject 1", - row1.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row1.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals(1, row1.get("Subject").size()); var row2 = page.getRows().get(1); Assert.assertEquals( "Sample B for subject 2", - row2.get("Sample").stream().findFirst().orElseThrow().getLabel()); + row2.get("Sample").stream().findFirst().orElseThrow().label()); Assert.assertEquals( Set.of("RNA-seq", "Whole genome sequencing"), - row2.get("Resource_analysisType").stream() - .map(ValueDTO::getLabel) - .collect(Collectors.toSet())); + row2.get("Resource_analysisType").stream().map(ValueDto::label).collect(Collectors.toSet())); } @Test @@ -353,7 +386,7 @@ public void testCountSamplesWithoutMaxDisplayCount() { var requestParams = new CountRequest(); requestParams.setView("Sample"); var result = sut.count(requestParams); - assertEquals(2, result.getCount()); + assertEquals(2, result.count()); } @Test @@ -361,7 +394,7 @@ public void testCountSubjectWithMaxDisplayCountLimitLessThanTotalCount() { var request = new CountRequest(); request.setView("Subject"); var result = sut.count(request); - Assert.assertEquals(1, result.getCount()); + Assert.assertEquals(1, result.count()); } @Test @@ -369,6 +402,6 @@ public void testCountResourceWithMaxDisplayCountLimitMoreThanTotalCount() { var request = new CountRequest(); request.setView("Resource"); var result = sut.count(request); - Assert.assertEquals(4, result.getCount()); + Assert.assertEquals(4, result.count()); } } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java index e929441fde..c7045c9bbf 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/SparqlQueryServiceTest.java @@ -1,44 +1,71 @@ package io.fairspace.saturn.services.views; -import java.io.*; -import java.util.*; +import java.io.IOException; +import java.util.Collections; +import java.util.List; import io.milton.http.ResourceFactory; -import io.milton.http.exceptions.*; -import io.milton.resource.*; -import org.apache.jena.query.*; -import org.apache.jena.rdf.model.*; -import org.apache.jena.sparql.core.*; -import org.apache.jena.sparql.util.*; -import org.eclipse.jetty.server.*; -import org.junit.*; -import org.junit.runner.*; -import org.mockito.*; -import org.mockito.junit.*; - -import io.fairspace.saturn.config.*; -import io.fairspace.saturn.rdf.dao.*; +import io.milton.http.exceptions.BadRequestException; +import io.milton.http.exceptions.ConflictException; +import io.milton.http.exceptions.NotAuthorizedException; +import io.milton.resource.MakeCollectionableResource; +import io.milton.resource.PutableResource; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.jena.query.Dataset; +import org.apache.jena.rdf.model.Model; +import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.apache.jena.sparql.core.DatasetImpl; +import org.apache.jena.sparql.util.Context; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.SearchProperties; +import io.fairspace.saturn.config.properties.StoreParamsProperties; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.controller.dto.request.CountRequest; +import io.fairspace.saturn.controller.dto.request.ViewRequest; +import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.search.FilteredDatasetGraph; -import io.fairspace.saturn.rdf.transactions.*; -import io.fairspace.saturn.services.metadata.*; -import io.fairspace.saturn.services.metadata.validation.*; -import io.fairspace.saturn.services.users.*; -import io.fairspace.saturn.services.workspaces.*; -import io.fairspace.saturn.webdav.*; +import io.fairspace.saturn.rdf.transactions.SimpleTransactions; +import io.fairspace.saturn.rdf.transactions.Transactions; +import io.fairspace.saturn.services.metadata.MetadataPermissions; +import io.fairspace.saturn.services.metadata.MetadataService; +import io.fairspace.saturn.services.metadata.validation.ComposedValidator; +import io.fairspace.saturn.services.metadata.validation.UniqueLabelValidator; +import io.fairspace.saturn.services.users.User; +import io.fairspace.saturn.services.users.UserService; +import io.fairspace.saturn.services.workspaces.Workspace; +import io.fairspace.saturn.services.workspaces.WorkspaceRole; +import io.fairspace.saturn.services.workspaces.WorkspaceService; +import io.fairspace.saturn.webdav.DavFactory; import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.TestUtils.*; -import static io.fairspace.saturn.auth.RequestContext.*; - -import static org.apache.jena.query.DatasetFactory.*; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.loadViewsConfig; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; +import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; + +import static org.apache.jena.query.DatasetFactory.wrap; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class SparqlQueryServiceTest { static final String BASE_PATH = "/api/webdav"; - static final String baseUri = ConfigLoader.CONFIG.publicUrl + BASE_PATH; + static final String baseUri = "http://localhost:8080" + BASE_PATH; static final String SAMPLE_NATURE_BLOOD = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl#C12434"; static final String ANALYSIS_TYPE_RNA_SEQ = "https://institut-curie.org/osiris#O6-12"; static final String ANALYSIS_TYPE_IMAGING = "https://institut-curie.org/osiris#C37-2"; @@ -56,79 +83,98 @@ public class SparqlQueryServiceTest { MetadataService api; QueryService queryService; + private DAO dao; + User user; - Authentication.User userAuthentication; User user2; - Authentication.User user2Authentication; User workspaceManager; - Authentication.User workspaceManagerAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; - - private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); - lenient().when(userService.currentUser()).thenReturn(user); - } + // TODO: move the selectUser methods to a parent with mocked UserService private void selectExternalUser() { - lenient().when(request.getAuthentication()).thenReturn(user2Authentication); + mockAuthentication("user2"); lenient().when(userService.currentUser()).thenReturn(user2); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } - private void setupUsers(Model model) { - userAuthentication = mockAuthentication("user"); - user = createTestUser("user", false); + private void selectRegularUser() { + mockAuthentication(USER); + lenient().when(userService.currentUser()).thenReturn(user); + } + + private void setupUsers() { + user = createTestUser(USER, false); user.setCanViewPublicMetadata(true); - new DAO(model).write(user); - user2Authentication = mockAuthentication("user2"); + dao.write(user); user2 = createTestUser("user2", false); - new DAO(model).write(user2); + dao.write(user2); workspaceManager = createTestUser("workspace-admin", false); - new DAO(model).write(workspaceManager); - workspaceManagerAuthentication = mockAuthentication("workspace-admin"); - adminAuthentication = mockAuthentication("admin"); - admin = createTestUser("admin", true); - new DAO(model).write(admin); + dao.write(workspaceManager); + admin = createTestUser(ADMIN, true); + dao.write(admin); } @Before public void before() throws NotAuthorizedException, BadRequestException, ConflictException, IOException { + JenaProperties.setMetadataBaseIRI("http://localhost/iri/"); var dsg = DatasetGraphFactory.createTxnMem(); Dataset ds = wrap(dsg); Transactions tx = new SimpleTransactions(ds); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + var systemVocabulary = model.read("system-vocabulary.ttl"); workspaceService = new WorkspaceService(tx, userService); var context = new Context(); - var davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); + var davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); var metadataPermissions = new MetadataPermissions(workspaceService, davFactory, userService); var filteredDatasetGraph = new FilteredDatasetGraph(ds.asDatasetGraph(), metadataPermissions); var filteredDataset = DatasetImpl.wrap(filteredDatasetGraph); + SearchProperties searchProperties = new SearchProperties(); + searchProperties.setCountRequestTimeout(60000); + searchProperties.setPageRequestTimeout(10000); + searchProperties.setMaxJoinItems(50); queryService = new SparqlQueryService( - ConfigLoader.CONFIG.search, loadViewsConfig("src/test/resources/test-views.yaml"), filteredDataset); + searchProperties, + new JenaProperties("http://localhost/iri/", new StoreParamsProperties()), + loadViewsConfig("src/test/resources/test-views.yaml"), + filteredDataset, + tx); when(permissions.canWriteMetadata(any())).thenReturn(true); - api = new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + api = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); + + dao = new DAO(model); - setupUsers(model); + setupUsers(); setupRequestContext(); - request = getCurrentRequest(); + HttpServletRequest request = getCurrentRequest(); selectAdmin(); var taxonomies = model.read("test-taxonomies.ttl"); api.put(taxonomies, Boolean.FALSE); - + when(userService.currentUser()).thenReturn(admin); var workspace = workspaceService.createWorkspace( Workspace.builder().code("Test").build()); workspaceService.setUserRole(workspace.getIri(), workspaceManager.getIri(), WorkspaceRole.Manager); @@ -161,24 +207,19 @@ public void testRetrieveSamplePage() { assertEquals(2, page.getRows().size()); // The implementation does not sort results. Probably deterministic, // but no certain order is guaranteed. - var row = page.getRows() - .get(0) - .get("Sample") - .iterator() - .next() - .getValue() - .equals("http://example.com/samples#s1-a") - ? page.getRows().get(0) - : page.getRows().get(1); + var row = + page.getRows().get(0).get("Sample").iterator().next().value().equals("http://example.com/samples#s1-a") + ? page.getRows().get(0) + : page.getRows().get(1); assertEquals( - "Sample A for subject 1", row.get("Sample").iterator().next().getLabel()); + "Sample A for subject 1", row.get("Sample").iterator().next().label()); assertEquals( - SAMPLE_NATURE_BLOOD, row.get("Sample_nature").iterator().next().getValue()); - assertEquals("Blood", row.get("Sample_nature").iterator().next().getLabel()); - assertEquals("Liver", row.get("Sample_topography").iterator().next().getLabel()); + SAMPLE_NATURE_BLOOD, row.get("Sample_nature").iterator().next().value()); + assertEquals("Blood", row.get("Sample_nature").iterator().next().label()); + assertEquals("Liver", row.get("Sample_topography").iterator().next().label()); assertEquals( 45.2f, - ((Number) row.get("Sample_tumorCellularity").iterator().next().getValue()).floatValue(), + ((Number) row.get("Sample_tumorCellularity").iterator().next().value()).floatValue(), 0.01); } @@ -188,7 +229,7 @@ public void testCountSamplesWithoutMaxDisplayCount() { var requestParams = new CountRequest(); requestParams.setView("Sample"); var result = queryService.count(requestParams); - assertEquals(2, result.getCount()); + assertEquals(2, result.count()); } @Test @@ -196,15 +237,17 @@ public void testCountSubjectWithMaxDisplayCountLimitLessThanTotalCount() { var request = new CountRequest(); request.setView("Subject"); var result = queryService.count(request); - Assert.assertEquals(1, result.getCount()); + Assert.assertEquals(1, result.count()); } @Test - public void testCountResourceWithMaxDisplayCountLimitMoreThanTotalCount() { + public void testCountResourceWithAccess() { + selectRegularUser(); var request = new CountRequest(); request.setView("Resource"); + var result = queryService.count(request); - Assert.assertEquals(3, result.getCount()); + Assert.assertEquals(3, result.count()); } @Test @@ -213,7 +256,7 @@ public void testCountSamplesWithoutViewAccess() { var countRequest = new CountRequest(); countRequest.setView("Sample"); var result = queryService.count(countRequest); - assertEquals(0, result.getCount()); + assertEquals(0, result.count()); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java index acfbb8ad95..7f488419bc 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/services/views/ViewServiceTest.java @@ -2,7 +2,7 @@ import java.io.IOException; import java.sql.SQLException; -import java.util.stream.Collectors; +import java.util.List; import io.milton.http.ResourceFactory; import io.milton.http.exceptions.BadRequestException; @@ -10,11 +10,11 @@ import io.milton.http.exceptions.NotAuthorizedException; import io.milton.resource.MakeCollectionableResource; import io.milton.resource.PutableResource; +import jakarta.servlet.http.HttpServletRequest; import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.core.DatasetGraphFactory; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -23,9 +23,11 @@ import org.mockito.junit.MockitoJUnitRunner; import io.fairspace.saturn.PostgresAwareTest; -import io.fairspace.saturn.config.Config; -import io.fairspace.saturn.config.ConfigLoader; -import io.fairspace.saturn.config.ViewsConfig; +import io.fairspace.saturn.auth.RequestContext; +import io.fairspace.saturn.config.properties.CacheProperties; +import io.fairspace.saturn.config.properties.ViewsProperties; +import io.fairspace.saturn.config.properties.WebDavProperties; +import io.fairspace.saturn.rdf.SparqlUtils; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.rdf.transactions.TxnIndexDatasetGraph; @@ -44,14 +46,13 @@ import static io.fairspace.saturn.TestUtils.createTestUser; import static io.fairspace.saturn.TestUtils.loadViewsConfig; -import static io.fairspace.saturn.TestUtils.mockAuthentication; import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static io.fairspace.saturn.services.views.ViewService.USER_DOES_NOT_HAVE_PERMISSIONS_TO_READ_FACETS; -import static org.apache.jena.query.DatasetFactory.wrap; +import static org.apache.jena.sparql.core.DatasetImpl.wrap; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -60,7 +61,7 @@ @RunWith(MockitoJUnitRunner.class) public class ViewServiceTest extends PostgresAwareTest { static final String BASE_PATH = "/api/webdav"; - static final String baseUri = ConfigLoader.CONFIG.publicUrl + BASE_PATH; + static final String baseUri = "http://localhost:8080" + BASE_PATH; @Mock BlobStore store; @@ -71,6 +72,9 @@ public class ViewServiceTest extends PostgresAwareTest { @Mock private MetadataPermissions permissions; + @Mock + private MaterializedViewService materializedViewService; + MetadataService api; ViewService viewService; @@ -78,16 +82,29 @@ public class ViewServiceTest extends PostgresAwareTest { public void before() throws SQLException, BadRequestException, ConflictException, NotAuthorizedException, IOException { var viewDatabase = buildViewDatabaseConfig(); - ViewsConfig config = loadViewsConfig("src/test/resources/test-views.yaml"); - var viewStoreClientFactory = new ViewStoreClientFactory(config, viewDatabase, new Config.Search()); - - var dsg = new TxnIndexDatasetGraph(DatasetGraphFactory.createTxnMem(), viewStoreClientFactory); + var viewsProperties = loadViewsConfig("src/test/resources/test-views.yaml"); + var configuration = new ViewStoreClient.ViewStoreConfiguration(viewsProperties); + var dataSource = getDataSource(viewDatabase); + var viewStoreClientFactory = new ViewStoreClientFactory( + viewsProperties, viewDatabase, materializedViewService, dataSource, configuration); + var dsg = new TxnIndexDatasetGraph( + viewsProperties, DatasetGraphFactory.createTxnMem(), viewStoreClientFactory, "http://localhost:8080"); Dataset ds = wrap(dsg); loadTestData(ds); - viewService = new ViewService(ConfigLoader.CONFIG, config, ds, viewStoreClientFactory, permissions); + var searchProperties = buildSearchProperties(); + var viewStoreReader = + new ViewStoreReader(searchProperties, viewsProperties, viewStoreClientFactory, configuration); + viewService = new ViewService( + searchProperties, + new CacheProperties(), + viewsProperties, + ds, + viewStoreReader, + viewStoreClientFactory, + permissions); } @Test @@ -95,12 +112,12 @@ public void testFetchViewConfig() { when(permissions.canReadFacets()).thenReturn(true); var facets = viewService.getFacets(); var dateFacets = facets.stream() - .filter(facet -> facet.getType() == ViewsConfig.ColumnType.Date) + .filter(facet -> facet.type() == ViewsProperties.ColumnType.Date) .toList(); Assert.assertEquals(2, dateFacets.size()); var boolFacets = facets.stream() - .filter(facet -> facet.getType() == ViewsConfig.ColumnType.Boolean) + .filter(facet -> facet.type() == ViewsProperties.ColumnType.Boolean) .toList(); Assert.assertEquals(1, boolFacets.size()); } @@ -118,23 +135,23 @@ public void testNoAccessExceptionFetchingFacetsWhenUserHasNoPermissions() { @Test public void testDisplayIndex_IsSet() { var views = viewService.getViews(); - var columns = views.get(1).getColumns().stream().toList(); + var columns = views.get(1).columns().stream().toList(); var selectedColumn = columns.stream() - .filter(c -> c.getTitle().equals("Morphology")) - .collect(Collectors.toList()) - .get(0); - Assert.assertEquals(Integer.valueOf(1), selectedColumn.getDisplayIndex()); + .filter(c -> c.title().equals("Morphology")) + .toList() + .getFirst(); + Assert.assertEquals(Integer.valueOf(1), selectedColumn.displayIndex()); } @Test public void testDisplayIndex_IsNotSet() { var views = viewService.getViews(); - var columns = views.get(1).getColumns().stream().toList(); + var columns = views.get(1).columns().stream().toList(); var selectedColumn = columns.stream() - .filter(c -> c.getTitle().equals("Laterality")) - .collect(Collectors.toList()) - .get(0); - Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), selectedColumn.getDisplayIndex()); + .filter(c -> c.title().equals("Laterality")) + .toList() + .getFirst(); + Assert.assertEquals(Integer.valueOf(Integer.MAX_VALUE), selectedColumn.displayIndex()); } @Test @@ -164,45 +181,51 @@ public void testFetchCachedViews() { verify(sut, never()).fetchViews(); } - private Config.ViewDatabase buildViewDatabaseConfig() { - var viewDatabase = new Config.ViewDatabase(); - viewDatabase.url = postgres.getJdbcUrl(); - viewDatabase.username = postgres.getUsername(); - viewDatabase.password = postgres.getPassword(); - viewDatabase.maxPoolSize = 5; - return viewDatabase; - } - private void loadTestData(Dataset ds) throws NotAuthorizedException, BadRequestException, ConflictException, IOException { // TODO: loaded data to be mocked instead of loading them this way Transactions tx = new SimpleTransactions(ds); Model model = ds.getDefaultModel(); var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + var systemVocabulary = model.read("system-vocabulary.ttl"); var workspaceService = new WorkspaceService(tx, userService); var context = new Context(); - var davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); + var davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); when(permissions.canWriteMetadata(any())).thenReturn(true); - api = new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + api = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); setupRequestContext(); + + var currentRequest = mock(HttpServletRequest.class); + RequestContext.setCurrentRequest(currentRequest); + RequestContext.setCurrentUserStringUri( + SparqlUtils.generateMetadataIriFromId("user").getURI()); var request = getCurrentRequest(); var taxonomies = model.read("test-taxonomies.ttl"); api.put(taxonomies, Boolean.TRUE); User user = createTestUser("user", true); - Authentication.User userAuthentication = mockAuthentication("admin"); - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); - lenient().when(userService.currentUser()).thenReturn(user); - + when(userService.currentUser()).thenReturn(user); var workspace = workspaceService.createWorkspace( Workspace.builder().code("Test").build()); - when(request.getHeader("Owner")).thenReturn(workspace.getIri().getURI()); when(request.getAttribute("BLOB")).thenReturn(new BlobInfo("id", 0, "md5")); diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/CollectionResourceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/CollectionResourceTest.java index cd171293c2..3cacb7360d 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/CollectionResourceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/CollectionResourceTest.java @@ -12,6 +12,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.services.metadata.MetadataService; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.vocabulary.FS; @@ -19,7 +20,7 @@ import io.fairspace.saturn.webdav.resources.CollectionResource; import static io.fairspace.saturn.TestUtils.setupRequestContext; -import static io.fairspace.saturn.config.Services.METADATA_SERVICE; +import static io.fairspace.saturn.config.MetadataConfig.METADATA_SERVICE; import static org.apache.jena.query.DatasetFactory.createTxnMem; import static org.junit.Assert.assertFalse; @@ -30,16 +31,16 @@ public class CollectionResourceTest { public static final String BASE_PATH = "/api/webdav"; private static final String baseUri = "http://example.com" + BASE_PATH; - private Model model = createTxnMem().getDefaultModel(); + private final Model model = createTxnMem().getDefaultModel(); private CollectionResource resource; - private Resource WORKSPACE_1 = model.createResource("http://localhost/iri/W1"); - private Resource WORKSPACE_2 = model.createResource("http://localhost/iri/W2"); - private Resource COLLECTION_1 = model.createResource("http://localhost/iri/C1"); - private Resource USER_1 = model.createResource("http://localhost/iri/userid1"); - private Resource USER_2 = model.createResource("http://localhost/iri/userid2"); - private Resource USER_3 = model.createResource("http://localhost/iri/userid3"); - private Resource USER_4 = model.createResource("http://localhost/iri/userid4"); + private final Resource WORKSPACE_1 = model.createResource("http://localhost/iri/W1"); + private final Resource WORKSPACE_2 = model.createResource("http://localhost/iri/W2"); + private final Resource COLLECTION_1 = model.createResource("http://localhost/iri/C1"); + private final Resource USER_1 = model.createResource("http://localhost/iri/userid1"); + private final Resource USER_2 = model.createResource("http://localhost/iri/userid2"); + private final Resource USER_3 = model.createResource("http://localhost/iri/userid3"); + private final Resource USER_4 = model.createResource("http://localhost/iri/userid4"); @Mock BlobStore store; @@ -50,6 +51,9 @@ public class CollectionResourceTest { @Mock MetadataService metadataService; + @Mock + WebDavProperties webDavProperties; + Context context = new Context(); @Before @@ -59,10 +63,18 @@ public void before() { .add(COLLECTION_1, RDF.type, FS.Collection) .add(COLLECTION_1, FS.ownedBy, WORKSPACE_1) .add(COLLECTION_1, FS.belongsTo, WORKSPACE_1); - + var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); context.set(METADATA_SERVICE, metadataService); - var factory = new DavFactory(model.createResource(baseUri), store, userService, context); - resource = new CollectionResource(factory, COLLECTION_1, Access.Manage); + var factory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + webDavProperties, + userVocabulary, + vocabulary); + resource = new CollectionResource(factory, COLLECTION_1, Access.Manage, userVocabulary, vocabulary); setupRequestContext(); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryAccessTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryAccessTest.java index 1ee2d38b2f..3e62e58745 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryAccessTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryAccessTest.java @@ -11,12 +11,12 @@ import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.Resource; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -28,12 +28,18 @@ import io.fairspace.saturn.vocabulary.FS; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static org.apache.jena.query.DatasetFactory.createTxnMem; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @RunWith(Parameterized.class) public class DavFactoryAccessTest { @@ -44,21 +50,19 @@ public class DavFactoryAccessTest { WorkspaceService workspaceService; User user; - Authentication.User userAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; private ResourceFactory factory; - private Dataset ds = createTxnMem(); - private Transactions tx = new SimpleTransactions(ds); - private Model model = ds.getDefaultModel(); + private final Dataset ds = createTxnMem(); + private final Transactions tx = new SimpleTransactions(ds); + private final Model model = ds.getDefaultModel(); + private final DAO dao = new DAO(model); - private Access grantedAccess; - private Status status; - private AccessMode accessMode; - private Access expectedAccess; - private Context context = new Context(); + private final Access grantedAccess; + private final Status status; + private final AccessMode accessMode; + private final Access expectedAccess; + private final Context context = new Context(); public DavFactoryAccessTest(Access grantedAccess, Status status, AccessMode accessMode, Access expectedAccess) { this.grantedAccess = grantedAccess; @@ -99,30 +103,37 @@ public static Iterable data() { } private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); + mockAuthentication(USER); lenient().when(userService.currentUser()).thenReturn(user); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } @Before public void before() { workspaceService = new WorkspaceService(tx, userService); - factory = new DavFactory(model.createResource(baseUri), store, userService, context); + var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + factory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); setupRequestContext(); - request = getCurrentRequest(); - userAuthentication = mockAuthentication("user"); + var request = getCurrentRequest(); user = createTestUser("user", false); user.setCanViewPublicData(true); user.setCanViewPublicMetadata(true); - new DAO(model).write(user); - adminAuthentication = mockAuthentication("admin"); + dao.write(user); admin = createTestUser("admin", true); - new DAO(model).write(admin); + dao.write(admin); selectAdmin(); var workspace = workspaceService.createWorkspace( diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryExtraStorageTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryExtraStorageTest.java index ea6c2c3c65..3d95ab4224 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryExtraStorageTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryExtraStorageTest.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Map; import javax.xml.namespace.QName; @@ -9,20 +10,26 @@ import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.ConflictException; import io.milton.http.exceptions.NotAuthorizedException; -import io.milton.resource.*; +import io.milton.resource.DeletableResource; +import io.milton.resource.FolderResource; +import io.milton.resource.GetableResource; +import io.milton.resource.MakeCollectionableResource; +import io.milton.resource.MultiNamespaceCustomPropertyResource; +import io.milton.resource.PostableResource; +import io.milton.resource.PutableResource; +import io.milton.resource.ReplaceableResource; +import jakarta.servlet.http.HttpServletRequest; import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.rdf.dao.DAO; -import io.fairspace.saturn.rdf.transactions.SimpleTransactions; -import io.fairspace.saturn.rdf.transactions.Transactions; import io.fairspace.saturn.services.users.User; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.vocabulary.FS; @@ -33,13 +40,27 @@ import io.fairspace.saturn.webdav.resources.ExtraStorageRootResource; import io.fairspace.saturn.webdav.resources.FileResource; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static java.lang.String.format; import static org.apache.jena.query.DatasetFactory.createTxnMem; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class DavFactoryExtraStorageTest { @@ -59,37 +80,47 @@ public class DavFactoryExtraStorageTest { Context context = new Context(); User user; - Authentication.User userAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; + private HttpServletRequest request; private ResourceFactory factory; - private Dataset ds = createTxnMem(); - private Transactions tx = new SimpleTransactions(ds); - private Model model = ds.getDefaultModel(); + private final Dataset ds = createTxnMem(); + private final Model model = ds.getDefaultModel(); private final String defaultExtraStorageRootName = "analysis-export"; private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); + mockAuthentication(USER); lenient().when(userService.currentUser()).thenReturn(user); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } @Before public void before() { - factory = new DavFactory(model.createResource(extraStorageUri), store, userService, context); + WebDavProperties webDavProperties = new WebDavProperties(); + webDavProperties.setBlobStorePath("db"); + var extraStorage = new WebDavProperties.ExtraStorage(); + extraStorage.setBlobStorePath("db/extra"); + extraStorage.setDefaultRootCollections(List.of(defaultExtraStorageRootName)); + webDavProperties.setExtraStorage(extraStorage); + var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + factory = new DavFactory( + model.createResource(extraStorageUri), + store, + userService, + context, + webDavProperties, + userVocabulary, + vocabulary); - userAuthentication = mockAuthentication("user"); user = createTestUser("user", false); new DAO(model).write(user); - adminAuthentication = mockAuthentication("admin"); admin = createTestUser("admin", true); new DAO(model).write(admin); @@ -213,9 +244,9 @@ public void testDeleteAllInFolder() throws NotAuthorizedException, BadRequestException, ConflictException, IOException { var root = (MakeCollectionableResource) factory.getResource(null, EXTRA_STORAGE_PATH); var coll = (FolderResource) root.createCollection(defaultExtraStorageRootName); - var file1 = coll.createNew("file1", input, FILE_SIZE, "text/abc"); - var file2 = coll.createNew("file2", input, FILE_SIZE, "text/abc"); - var file3 = coll.createNew("file3", input, FILE_SIZE, "text/abc"); + coll.createNew("file1", input, FILE_SIZE, "text/abc"); + coll.createNew("file2", input, FILE_SIZE, "text/abc"); + coll.createNew("file3", input, FILE_SIZE, "text/abc"); ((PostableResource) root.child(defaultExtraStorageRootName)) .processForm(Map.of("action", "delete_all_in_directory"), Map.of()); @@ -232,7 +263,7 @@ public void testDeleteAllInFolder() public void testThrowsErrorWhenNonDefaultRootDirectoryName() throws NotAuthorizedException, BadRequestException, ConflictException { var root = (MakeCollectionableResource) factory.getResource(null, EXTRA_STORAGE_PATH); - var coll = (FolderResource) root.createCollection("coll1"); + root.createCollection("coll1"); } @Test() diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryTest.java index 28eade87ea..1a9530f1e2 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DavFactoryTest.java @@ -10,21 +10,30 @@ import io.milton.http.exceptions.BadRequestException; import io.milton.http.exceptions.ConflictException; import io.milton.http.exceptions.NotAuthorizedException; -import io.milton.resource.*; +import io.milton.resource.DeletableResource; +import io.milton.resource.FolderResource; +import io.milton.resource.GetableResource; +import io.milton.resource.MakeCollectionableResource; +import io.milton.resource.MoveableResource; +import io.milton.resource.MultiNamespaceCustomPropertyResource; +import io.milton.resource.PostableResource; +import io.milton.resource.PutableResource; +import io.milton.resource.ReplaceableResource; +import jakarta.servlet.http.HttpServletRequest; import org.apache.jena.query.Dataset; import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.util.Context; -import org.eclipse.jetty.server.Authentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.JenaProperties; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; -import io.fairspace.saturn.services.metadata.MetadataService; import io.fairspace.saturn.services.users.User; import io.fairspace.saturn.services.users.UserService; import io.fairspace.saturn.services.workspaces.Workspace; @@ -34,15 +43,26 @@ import io.fairspace.saturn.webdav.blobstore.BlobInfo; import io.fairspace.saturn.webdav.blobstore.BlobStore; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.ADMIN; +import static io.fairspace.saturn.TestUtils.USER; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; import static io.milton.http.ResponseStatus.SC_FORBIDDEN; import static java.lang.String.format; import static org.apache.jena.query.DatasetFactory.createTxnMem; import static org.apache.jena.rdf.model.ResourceFactory.createResource; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class DavFactoryTest { @@ -60,55 +80,57 @@ public class DavFactoryTest { @Mock UserService userService; - @Mock - MetadataService metadataService; - WorkspaceService workspaceService; Workspace workspace; Context context = new Context(); User user; - Authentication.User userAuthentication; User workspaceManager; - Authentication.User workspaceManagerAuthentication; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; + private HttpServletRequest request; private ResourceFactory factory; - private Dataset ds = createTxnMem(); - private Transactions tx = new SimpleTransactions(ds); - private Model model = ds.getDefaultModel(); + private final Dataset ds = createTxnMem(); + private final Transactions tx = new SimpleTransactions(ds); + private final Model model = ds.getDefaultModel(); + private final DAO dao = new DAO(model); private void selectRegularUser() { - lenient().when(request.getAuthentication()).thenReturn(userAuthentication); + mockAuthentication(USER); lenient().when(userService.currentUser()).thenReturn(user); } private void selectWorkspaceManager() { - lenient().when(request.getAuthentication()).thenReturn(workspaceManagerAuthentication); + mockAuthentication("workspace-admin"); lenient().when(userService.currentUser()).thenReturn(workspaceManager); } private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); + mockAuthentication(ADMIN); lenient().when(userService.currentUser()).thenReturn(admin); } @Before public void before() { + JenaProperties.setMetadataBaseIRI("http://localhost/iri/"); workspaceService = new WorkspaceService(tx, userService); - factory = new DavFactory(model.createResource(baseUri), store, userService, context); + var vocabulary = model.read("test-vocabulary.ttl"); + var userVocabulary = model.read("vocabulary.ttl"); + factory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); - userAuthentication = mockAuthentication("user"); user = createTestUser("user", false); - new DAO(model).write(user); + dao.write(user); workspaceManager = createTestUser("workspace-admin", false); - new DAO(model).write(workspaceManager); - workspaceManagerAuthentication = mockAuthentication("workspace-admin"); - adminAuthentication = mockAuthentication("admin"); + dao.write(workspaceManager); admin = createTestUser("admin", true); - new DAO(model).write(admin); + dao.write(admin); setupRequestContext(); request = getCurrentRequest(); @@ -427,36 +449,36 @@ public void testPurgeFile() throws NotAuthorizedException, BadRequestException, } @Test - public void testRenameFile() throws NotAuthorizedException, BadRequestException, ConflictException, IOException { + public void testRenameDirectory() + throws NotAuthorizedException, BadRequestException, ConflictException, IOException { var root = (MakeCollectionableResource) factory.getResource(null, BASE_PATH); var coll = (FolderResource) root.createCollection("coll"); - var file = coll.createNew("old", input, FILE_SIZE, "text/abc"); + var dir = coll.createCollection("old"); + ((FolderResource) dir).createNew("file", input, FILE_SIZE, "text/abc"); - ((MoveableResource) file).moveTo(coll, "new"); + ((MoveableResource) dir).moveTo(coll, "new"); assertEquals(1, coll.getChildren().size()); assertNull(coll.child("old")); assertNotNull(coll.child("new")); + + assertNull(factory.getResource(null, BASE_PATH + "/coll/old/file")); + assertNotNull(factory.getResource(null, BASE_PATH + "/coll/new/file")); } @Test - public void testRenameDirectory() - throws NotAuthorizedException, BadRequestException, ConflictException, IOException { + public void testRenameFile() throws NotAuthorizedException, BadRequestException, ConflictException, IOException { var root = (MakeCollectionableResource) factory.getResource(null, BASE_PATH); var coll = (FolderResource) root.createCollection("coll"); - var dir = coll.createCollection("old"); - ((FolderResource) dir).createNew("file", input, FILE_SIZE, "text/abc"); + var file = coll.createNew("old", input, FILE_SIZE, "text/abc"); - ((MoveableResource) dir).moveTo(coll, "new"); + ((MoveableResource) file).moveTo(coll, "new"); assertEquals(1, coll.getChildren().size()); assertNull(coll.child("old")); assertNotNull(coll.child("new")); - - assertNull(factory.getResource(null, BASE_PATH + "/coll/old/file")); - assertNotNull(factory.getResource(null, BASE_PATH + "/coll/new/file")); } @Test diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DirectoryResourceTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DirectoryResourceTest.java index 1b06d9aa0c..192d999c49 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DirectoryResourceTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/DirectoryResourceTest.java @@ -3,6 +3,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.Map; import io.milton.http.FileItem; @@ -19,13 +20,13 @@ import org.apache.jena.sparql.util.Context; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; -import org.eclipse.jetty.server.Authentication; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import io.fairspace.saturn.config.properties.WebDavProperties; import io.fairspace.saturn.rdf.dao.DAO; import io.fairspace.saturn.rdf.transactions.SimpleTransactions; import io.fairspace.saturn.rdf.transactions.Transactions; @@ -44,15 +45,21 @@ import io.fairspace.saturn.webdav.resources.DirectoryResource; import io.fairspace.saturn.webdav.resources.FileResource; -import static io.fairspace.saturn.TestUtils.*; +import static io.fairspace.saturn.TestUtils.createTestUser; +import static io.fairspace.saturn.TestUtils.mockAuthentication; +import static io.fairspace.saturn.TestUtils.setupRequestContext; import static io.fairspace.saturn.auth.RequestContext.getCurrentRequest; -import static io.fairspace.saturn.config.Services.METADATA_SERVICE; +import static io.fairspace.saturn.config.MetadataConfig.METADATA_SERVICE; import static org.apache.jena.query.DatasetFactory.wrap; import static org.apache.jena.rdf.model.ResourceFactory.createProperty; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class DirectoryResourceTest { @@ -85,11 +92,12 @@ public class DirectoryResourceTest { private DavFactory davFactory; DirectoryResource dir; User admin; - Authentication.User adminAuthentication; - private org.eclipse.jetty.server.Request request; + + Model vocabulary; + Model systemVocabulary; + Model userVocabulary; private void selectAdmin() { - lenient().when(request.getAuthentication()).thenReturn(adminAuthentication); lenient().when(userService.currentUser()).thenReturn(admin); } @@ -99,22 +107,34 @@ public void before() throws NotAuthorizedException, BadRequestException, Conflic Dataset ds = wrap(dsg); Transactions tx = new SimpleTransactions(ds); model = ds.getDefaultModel(); + vocabulary = model.read("test-vocabulary.ttl"); + systemVocabulary = model.read("system-vocabulary.ttl"); + userVocabulary = model.read("vocabulary.ttl"); var workspaceService = new WorkspaceService(tx, userService); - var vocabulary = model.read("test-vocabulary.ttl"); - when(permissions.canWriteMetadata(any())).thenReturn(true); Context context = new Context(); - metadataService = - new MetadataService(tx, vocabulary, new ComposedValidator(new UniqueLabelValidator()), permissions); + metadataService = new MetadataService( + tx, + vocabulary, + systemVocabulary, + new ComposedValidator(List.of(new UniqueLabelValidator())), + permissions); context.set(METADATA_SERVICE, metadataService); - davFactory = new DavFactory(model.createResource(baseUri), store, userService, context); - - adminAuthentication = mockAuthentication("admin"); + davFactory = new DavFactory( + model.createResource(baseUri), + store, + userService, + context, + new WebDavProperties(), + userVocabulary, + vocabulary); + + mockAuthentication("admin"); admin = createTestUser("admin", true); new DAO(model).write(admin); setupRequestContext(); - request = getCurrentRequest(); + var request = getCurrentRequest(); selectAdmin(); @@ -140,7 +160,8 @@ public void before() throws NotAuthorizedException, BadRequestException, Conflic @Test public void testFileUploadSuccess() throws NotAuthorizedException, ConflictException, BadRequestException { - dir = new DirectoryResource(davFactory, model.getResource(baseUri + "/dir"), Access.Manage); + dir = new DirectoryResource( + davFactory, model.getResource(baseUri + "/dir"), Access.Manage, userVocabulary, vocabulary); dir.subject.addProperty(RDF.type, FS.Directory); dir.processForm(Map.of("action", "upload_files"), Map.of("/subdir/file.ext", blobFileItem)); @@ -159,7 +180,8 @@ public void testFileUploadSuccess() throws NotAuthorizedException, ConflictExcep @Test public void testDeleteAllInDirectory() throws NotAuthorizedException, ConflictException, BadRequestException, IOException { - dir = new DirectoryResource(davFactory, model.getResource(baseUri + "/dir"), Access.Manage); + dir = new DirectoryResource( + davFactory, model.getResource(baseUri + "/dir"), Access.Manage, userVocabulary, vocabulary); dir.subject.addProperty(RDF.type, FS.Directory); dir.createNew("file1", input, 3L, "text/abc"); @@ -194,7 +216,12 @@ public void testFileUploadExistingDir() throws NotAuthorizedException, ConflictE @Test public void testTypedLiteralMetadataUploadSuccess() throws NotAuthorizedException, ConflictException, BadRequestException { - String csv = "Path,Description\n" + ".,\"Blah\"\n" + "./coffee.jpg,\"Blah blah\"\n"; + String csv = + """ + Path,Description + .,"Blah" + ./coffee.jpg,"Blah blah" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); DirectoryResource dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); dir.processForm(Map.of("action", "upload_metadata"), Map.of("file", file)); @@ -208,7 +235,10 @@ public void testTypedLiteralMetadataUploadSuccess() @Test(expected = BadRequestException.class) public void testMetadataUploadUnknownProperty() throws NotAuthorizedException, ConflictException, BadRequestException { - String csv = "Path,Unknown\n" + "./coll1,\"Blah blah\"\n"; + String csv = """ + Path,Unknown + ./coll1,"Blah blah" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); @@ -217,7 +247,10 @@ public void testMetadataUploadUnknownProperty() @Test(expected = BadRequestException.class) public void testMetadataUploadEmptyHeader() throws NotAuthorizedException, ConflictException, BadRequestException { - String csv = ",\n" + "./coll1,\"Blah blah\"\n"; + String csv = """ + , + ./coll1,"Blah blah" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); @@ -226,7 +259,10 @@ public void testMetadataUploadEmptyHeader() throws NotAuthorizedException, Confl @Test(expected = BadRequestException.class) public void testMetadataUploadUnknownFile() throws NotAuthorizedException, ConflictException, BadRequestException { - String csv = "Path,Description\n" + "./subdir,\"Blah blah\"\n"; + String csv = """ + Path,Description + ./subdir,"Blah blah" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); @@ -235,7 +271,10 @@ public void testMetadataUploadUnknownFile() throws NotAuthorizedException, Confl @Test(expected = BadRequestException.class) public void testMetadataUploadDeletedFile() throws NotAuthorizedException, ConflictException, BadRequestException { - String csv = "Path,Description\n" + "./subdir,\"Blah blah\"\n"; + String csv = """ + Path,Description + ./subdir,"Blah blah" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); var subdir = (DirectoryResource) dir.createCollection("subdir"); @@ -251,7 +290,11 @@ public void testLinkedMetadataUploadByIRISuccess() dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); assert !dir.subject.hasProperty(sampleProp); - String csv = "Path,Is about biological sample\n" + ".,\"http://example.com/samples#s2-b\"\n"; + String csv = + """ + Path,Is about biological sample + .,"http://example.com/samples#s2-b" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir.processForm(Map.of("action", "upload_metadata"), Map.of("file", file)); @@ -265,7 +308,11 @@ public void testLinkedMetadataUploadByLabelSuccess() dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); assert !dir.subject.hasProperty(sampleProp); - String csv = "Path,Is about biological sample\n" + ".,\"Sample A for subject 1\"\n"; + String csv = + """ + Path,Is about biological sample + .,"Sample A for subject 1" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir.processForm(Map.of("action", "upload_metadata"), Map.of("file", file)); @@ -279,7 +326,11 @@ public void testLinkedMetadataUploadByUnknownIRI() dir = (DirectoryResource) davFactory.getResource(null, BASE_PATH + "/coll1"); assert !dir.subject.hasProperty(sampleProp); - String csv = "Path,Is about biological sample\n" + ".,\"http://example.com/samples#unknown-sample\"\n"; + String csv = + """ + Path,Is about biological sample + .,"http://example.com/samples#unknown-sample" + """; when(file.getInputStream()).thenReturn(new ByteArrayInputStream(csv.getBytes())); dir.processForm(Map.of("action", "upload_metadata"), Map.of("file", file)); } diff --git a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/WebDAVServletTest.java b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/WebDAVServletTest.java index 3df722cec5..33f58babae 100644 --- a/projects/saturn/src/test/java/io/fairspace/saturn/webdav/WebDAVServletTest.java +++ b/projects/saturn/src/test/java/io/fairspace/saturn/webdav/WebDAVServletTest.java @@ -2,14 +2,14 @@ import java.io.IOException; import java.util.Vector; -import javax.servlet.ServletInputStream; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import com.pivovarit.function.ThrowingConsumer; import io.milton.http.ResourceFactory; import io.milton.resource.FolderResource; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,7 +23,11 @@ import static io.fairspace.saturn.TestUtils.setupRequestContext; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class WebDAVServletTest { @@ -126,6 +130,7 @@ public void testGetPayloadIsReadOutsideTransaction() throws Exception { var order = inOrder(txn, resource); order.verify(txn).executeRead(any()); + // Called after the transaction is finished order.verify(resource).sendContent(eq(out), any(), any(), any()); } diff --git a/projects/saturn/vocabulary.ttl b/projects/saturn/vocabulary.ttl deleted file mode 100644 index 5ef0bcd9a2..0000000000 --- a/projects/saturn/vocabulary.ttl +++ /dev/null @@ -1,842 +0,0 @@ -@prefix fs: . -@prefix owl: . -@prefix rdf: . -@prefix rdfs: . -@prefix sh: . -@prefix xsd: . -@prefix schema: . -@prefix foaf: . -@prefix dash: . -@prefix curie: . - -######################## -### User shapes ### -######################## - -curie:aboutSubject a rdf:Property . -curie:aboutEvent a rdf:Property . -curie:sample a rdf:Property . -curie:analysisType a rdf:Property . -curie:workspaceType a rdf:Property . -curie:principalInvestigator a rdf:Property . -curie:projectCode a rdf:Property . -curie:analysisCode a rdf:Property . -curie:analysisDate a rdf:Property . -curie:platformName a rdf:Property . -curie:analyticPipelineCode a rdf:Property . - -curie:WorkspaceType a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The type of the workspace. " ; - sh:name "Workspace type" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique workspace type label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -## Augmented system class shapes -fs:Workspace sh:property - [ - sh:name "Type" ; - sh:description "Workspace type." ; - sh:maxCount 1 ; - sh:class curie:WorkspaceType ; - sh:path curie:workspaceType - ], - [ - sh:name "Project code" ; - sh:description "Project code related to the workspace." ; - sh:datatype xsd:string ; - dash:singleLine true ; - sh:maxCount 1 ; - sh:path curie:projectCode - ], - [ - sh:name "Principal investigator" ; - sh:description "Name of the PI or team leader." ; - sh:datatype xsd:string ; - dash:singleLine true ; - sh:maxCount 1 ; - sh:path curie:principalInvestigator - ] . - -fs:File sh:property - [ - sh:name "Is about subject" ; - sh:description "Subjects that are featured in this file." ; - sh:class curie:Subject ; - sh:path curie:aboutSubject - ], - [ - sh:name "Is about biological sample" ; - sh:description "Biological samples that are featured in this file." ; - sh:class curie:BiologicalSample ; - sh:path curie:sample - ], - [ - sh:name "Is about tumor pathology event" ; - sh:description "Events that are featured in this file." ; - sh:class curie:TumorPathologyEvent ; - sh:path curie:aboutEvent - ], - [ - sh:name "Type of analysis" ; - sh:description "Type of analysis associated to this file" ; - sh:maxCount 1 ; - sh:class curie:AnalysisType ; - sh:path curie:analysisType - ], - [ - sh:name "Analysis code" ; - sh:description "The analysis code (could be the sequencing run identifier for instance, or the imaging identifier, etc.)" ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path curie:analysisCode - ], - [ - sh:name "Analysis date" ; - sh:description "" ; - sh:datatype xsd:date ; - sh:maxCount 1 ; - sh:path curie:analysisDate - ], - [ - sh:name "Platform name" ; - sh:description "The name of the technology platform on which the analysis has been processed." ; - sh:class curie:TechnologyPlatformName ; - sh:maxCount 1 ; - sh:path curie:platformName - ], - [ - sh:name "Analytic pipeline code" ; - sh:description "The analytic pipeline code (which is helpful for data traceability and comparison)." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path curie:analyticPipelineCode - ] . - -fs:Directory sh:property - [ - sh:name "Is about subject" ; - sh:description "Subjects that are featured in this directory." ; - sh:class curie:Subject ; - sh:path curie:aboutSubject - ], - [ - sh:name "Is about biological sample" ; - sh:description "Biological samples that are featured in this directory." ; - sh:class curie:BiologicalSample ; - sh:path curie:sample - ], - [ - sh:name "Is about tumor pathology event" ; - sh:description "Events that are featured in this directory." ; - sh:class curie:TumorPathologyEvent ; - sh:path curie:aboutEvent - ], - [ - sh:name "Type of analysis" ; - sh:description "Type of analysis associated to this directory" ; - sh:class curie:AnalysisType ; - sh:path curie:analysisType - ] . - -fs:Collection sh:property - [ - sh:name "Is about subject" ; - sh:description "Subjects that are featured in this collection." ; - sh:class curie:Subject ; - sh:path curie:aboutSubject - ], - [ - sh:name "Is about biological sample" ; - sh:description "Biological samples that are featured in this collection." ; - sh:class curie:BiologicalSample ; - sh:path curie:sample - ], - [ - sh:name "Is about tumor pathology event" ; - sh:description "Events that are featured in this collection." ; - sh:class curie:TumorPathologyEvent ; - sh:path curie:aboutEvent - ], - [ - sh:name "Type of analysis" ; - sh:description "Type of analysis associated to this collection" ; - sh:class curie:AnalysisType ; - sh:path curie:analysisType - ] . - -## User class Shapes - -curie:AnalysisType a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The type of analysis." ; - sh:name "Analysis type" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique analysis type label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TechnologyPlatformName a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "" ; - sh:name "Technology platform name" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique technology platform name." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:Gender a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The gender of the subject." ; - sh:name "Gender" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique gender label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:Species a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The species of the subject." ; - sh:name "Species" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique species label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:AvailabilityForResearch a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "Indication whether the subject is available for research." ; - sh:name "Availability for research" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique availability label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:ConsentAnswer a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "Answer type for consent questions." ; - sh:name "Consent answer" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique answer label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:ageAtLastNews a rdf:Property . -curie:ageAtDeath a rdf:Property . -curie:isOfGender a rdf:Property . -curie:isOfSpecies a rdf:Property . -curie:availableForResearch a rdf:Property . -curie:dateOfOpposition a rdf:Property . -curie:reuseClinicalWithGeneticData a rdf:Property . -curie:sampleStorageAndReuse a rdf:Property . -curie:geneticsAnalysis a rdf:Property . - -curie:Subject a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "A subject of research." ; - sh:name "Subject" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Gender" ; - sh:description "The gender of the subject." ; - sh:maxCount 1 ; - sh:class curie:Gender ; - sh:path curie:isOfGender - ], - [ - sh:name "Species" ; - sh:description "The species of the subject." ; - sh:maxCount 1 ; - sh:class curie:Species ; - sh:path curie:isOfSpecies - ], - [ - sh:name "Age at last news" ; - sh:description "The age at last news." ; - sh:datatype xsd:integer ; - sh:maxCount 1 ; - sh:path curie:ageAtLastNews - ], - [ - sh:name "Age at death" ; - sh:description "The age at death." ; - sh:datatype xsd:integer ; - sh:maxCount 1 ; - sh:path curie:ageAtDeath - ], - [ - sh:name "Available for research" ; - sh:maxCount 1 ; - sh:class curie:AvailabilityForResearch ; - sh:path curie:availableForResearch - ], - [ - sh:name "Date of opposition" ; - sh:datatype xsd:date ; - sh:maxCount 1 ; - sh:path curie:dateOfOpposition - ], - [ - sh:name "Reuse clinical with genetic data" ; - sh:maxCount 1 ; - sh:class curie:ConsentAnswer ; - sh:path curie:reuseClinicalWithGeneticData - ], - [ - sh:name "Sample storage and reuse" ; - sh:maxCount 1 ; - sh:class curie:ConsentAnswer ; - sh:path curie:sampleStorageAndReuse - ], - [ - sh:name "Genetic analysis" ; - sh:maxCount 1 ; - sh:class curie:ConsentAnswer ; - sh:path curie:geneticAnalysis - ], - [ - sh:name "Label" ; - sh:description "Unique person label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label; - sh:order 0 - ], - [ - sh:name "Description" ; - sh:description "" ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:comment - ], - [ - sh:name "Tumor pathology events" ; - sh:description "Tumor pathology events" ; - sh:path [sh:inversePath curie:eventSubject]; - ], - [ - sh:name "Samples" ; - sh:description "Samples" ; - sh:path [sh:inversePath curie:subject]; - ], - [ - sh:name "Files" ; - sh:description "Linked files" ; - sh:path [sh:inversePath curie:aboutSubject]; - ]. - - -curie:Topography a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The topography of the tumor coded in ICD-10 (subdivision ICD-O-3)." ; - sh:name "Topography" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique topography label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:Morphology a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The morphology of the tumor coded in ICD-10 (subdivision ICD-O-3)." ; - sh:name "Morphology" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique morphology label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:Laterality a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The laterality of the tumor." ; - sh:name "Laterality" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique laterality label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:EventType a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The type of tumor pathology event." ; - sh:name "Event type" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique event type label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TumorGradeType a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The type of grading classification." ; - sh:name "Tumor grade type" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique tumor grade type label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TumorGradeValue a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The grading value." ; - sh:name "Tumor grade value" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique tumor grade value label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TnmT a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The primary tumor size." ; - sh:name "TNM_T" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique TNM_T label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TnmN a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description " The regional lymph nodes." ; - sh:name "TNM_N" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique TNM_N label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:TnmM a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description " Distant metastasis." ; - sh:name "TNM_M" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique TNM_M label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:eventType a rdf:Property . -curie:topography a rdf:Property . -curie:tumorMorphology a rdf:Property . -curie:tumorLaterality a rdf:Property . -curie:yearOfDiagnosis a rdf:Property . -curie:ageAtDiagnosis a rdf:Property . -curie:tumorGradeType a rdf:Property . -curie:tumorGradeValue a rdf:Property . -curie:cTnmT a rdf:Property . -curie:cTnmN a rdf:Property . -curie:cTnmM a rdf:Property . -curie:pTnmT a rdf:Property . -curie:pTnmN a rdf:Property . -curie:pTnmM a rdf:Property . -curie:yTnmT a rdf:Property . -curie:yTnmN a rdf:Property . -curie:yTnmM a rdf:Property . - -curie:isAnalysedBy a rdf:Property . -curie:eventSubject a rdf:Property . -curie:isLinkedTo a rdf:Property . - -curie:TumorPathologyEvent a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "" ; - sh:name "Tumor pathology event" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Topography" ; - sh:description "The topography of the tumor." ; - sh:class curie:Topography ; - sh:path curie:topography - ], - [ - sh:name "Morphology" ; - sh:description "The morphology of the tumor." ; - sh:class curie:Morphology ; - sh:path curie:tumorMorphology - ], - [ - sh:name "Laterality" ; - sh:description "The laterality of the tumor." ; - sh:maxCount 1 ; - sh:class curie:Laterality ; - sh:path curie:tumorLaterality - ], - [ - sh:name "Event type" ; - sh:description "The type of tumor pathology event." ; - sh:maxCount 1 ; - sh:class curie:EventType ; - sh:path curie:eventType - ], - [ - sh:name "Year of diagnosis" ; - sh:description "The diagnosis year of the primary tumor." ; - sh:datatype xsd:integer ; - sh:maxCount 1 ; - sh:path curie:yearOfDiagnosis - ], - [ - sh:name "Age at diagnosis" ; - sh:description "The age at diagnosis." ; - sh:datatype xsd:integer ; - sh:maxCount 1 ; - sh:path curie:ageAtDiagnosis - ], - [ - sh:name "Tumor grade type" ; - sh:description "The type of tumor grading classification." ; - sh:class curie:TumorGradeType ; - sh:maxCount 1 ; - sh:path curie:tumorGradeType - ], - [ - sh:name "Tumor grade value" ; - sh:description "The tumor grading value." ; - sh:class curie:TumorGradeValue ; - sh:maxCount 1 ; - sh:path curie:tumorGradeValue - ], - [ - sh:name "cTNM_T" ; - sh:description "The primary tumor size (clinical evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmT ; - sh:path curie:cTnmT - ], - [ - sh:name "cTNM_N" ; - sh:description "The regional lymph nodes (clinical evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmN ; - sh:path curie:cTnmN - ], - [ - sh:name "cTNM_M" ; - sh:description "Distant metastasis (clinical evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmM ; - sh:path curie:cTnmM - ], - [ - sh:name "pTNM_T" ; - sh:description "The primary tumor size (pathological evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmT ; - sh:path curie:pTnmT - ], - [ - sh:name "pTNM_N" ; - sh:description "The regional lymph nodes (pathological evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmN ; - sh:path curie:pTnmN - ], - [ - sh:name "pTNM_M" ; - sh:description "Distant metastasis (pathological evaluation)." ; - sh:maxCount 1 ; - sh:class curie:TnmM ; - sh:path curie:pTnmM - ], - [ - sh:name "yTNM_T" ; - sh:description "The primary tumor size (after treatment)." ; - sh:maxCount 1 ; - sh:class curie:TnmT ; - sh:path curie:yTnmT - ], - [ - sh:name "yTNM_N" ; - sh:description "The regional lymph nodes (after treatment)." ; - sh:maxCount 1 ; - sh:class curie:TnmN ; - sh:path curie:yTnmN - ], - [ - sh:name "yTNM_M" ; - sh:description "Distant metastasis (after treatment)." ; - sh:maxCount 1 ; - sh:class curie:TnmM ; - sh:path curie:yTnmM - ], - [ - sh:name "Event subject" ; - sh:description "The subject associated with this event." ; - sh:class curie:Subject ; - sh:maxCount 1 ; - sh:path curie:eventSubject - ], - [ - sh:name "Label" ; - sh:description "Unique tumor pathology event label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label; - sh:order 0 - ], - [ - sh:name "Description" ; - sh:description "" ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:comment - ], - [ - sh:name "Samples" ; - sh:description "Samples" ; - sh:path [sh:inversePath curie:diagnosis]; - ], - [ - sh:name "Files" ; - sh:description "Linked files" ; - sh:path [sh:inversePath curie:aboutEvent]; - ]. - - -curie:SampleNature a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The sample nature." ; - sh:name "Sample nature" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique sample nature label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:SampleOrigin a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "The sample origin." ; - sh:name "Sample origin" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Label" ; - sh:description "Unique sample origin label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label - ] . - -curie:collectDate a rdf:Property . -curie:tumorCellularity a rdf:Property . -curie:isOfNature a rdf:Property . -curie:parentIsOfNature a rdf:Property . -curie:hasOrigin a rdf:Property . -curie:subject a rdf:Property . -curie:diagnosis a rdf:Property . -curie:isChildOf a rdf:Property . - -curie:BiologicalSample a rdfs:Class, sh:NodeShape ; - sh:closed false ; - sh:description "" ; - sh:name "Biological sample" ; - sh:ignoredProperties ( rdf:type owl:sameAs ) ; - sh:property - [ - sh:name "Collect date" ; - sh:description "The collect date of the biological sample." ; - sh:datatype xsd:date ; - sh:maxCount 1 ; - sh:path curie:collectDate - ], - [ - sh:name "Tumor cellularity" ; - sh:description "The percentage of tumor cells in the biological sample (pathological measure)." ; - sh:datatype xsd:integer ; - sh:maxCount 1 ; - sh:path curie:tumorCellularity - ], - [ - sh:name "Topography" ; - sh:description "The topography of the sample." ; - sh:maxCount 1 ; - sh:class curie:Topography ; - sh:path curie:topography - ], - [ - sh:name "Sample nature" ; - sh:description "The sample nature." ; - sh:maxCount 1 ; - sh:class curie:SampleNature ; - sh:path curie:isOfNature - ], - [ - sh:name "Parent sample nature" ; - sh:description "Natures of parent samples." ; - sh:class curie:SampleNature ; - sh:path curie:parentIsOfNature - ], - [ - sh:name "Sample origin" ; - sh:description "The sample origin." ; - sh:maxCount 1 ; - sh:class curie:SampleOrigin ; - sh:path curie:hasOrigin ; - ], - [ - sh:name "Subject" ; - sh:description "The subject associated with this sample." ; - sh:class curie:Subject ; - sh:maxCount 1 ; - sh:path curie:subject - ], - [ - sh:name "Diagnosis" ; - sh:description "The diagnosing tumor pathology event." ; - sh:class curie:TumorPathologyEvent ; - sh:maxCount 1 ; - sh:path curie:diagnosis - ], - [ - sh:name "Sample is child sample of" ; - sh:description "The biological sample has been extracted from this parent sample." ; - sh:class curie:BiologicalSample ; - sh:path curie:isChildOf - ], - [ - sh:name "Label" ; - sh:description "Unique biological sample label." ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - dash:singleLine true ; - fs:importantProperty true ; - sh:path rdfs:label; - sh:order 0 - ], - [ - sh:name "Description" ; - sh:description "" ; - sh:datatype xsd:string ; - sh:maxCount 1 ; - sh:path rdfs:comment - ], - [ - sh:name "Child samples" ; - sh:description "Child samples" ; - sh:path [sh:inversePath curie:isChildOf]; - ], - [ - sh:name "Files" ; - sh:description "Linked files" ; - sh:path [sh:inversePath curie:sample]; - ].