diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cbc162e83e..5d8fe3542c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -48,7 +48,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -59,7 +59,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl diff --git a/.github/workflows/lint_openapi.yml b/.github/workflows/lint_openapi.yml index 260b951f1d..16518f6b42 100644 --- a/.github/workflows/lint_openapi.yml +++ b/.github/workflows/lint_openapi.yml @@ -3,6 +3,7 @@ on: pull_request: paths: - "openapi.yaml" + - "backend/src/main/resources/com/bakdata/conquery/external/openapi-form-backend.yaml" - ".github/workflows/lint_openapi.yaml" jobs: lint: @@ -11,5 +12,7 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 - - name: "Lint OpenApi spec" + - name: "Lint OpenApi spec for /api" run: vacuum lint --details ./openapi.yaml + - name: "Lint OpenApi spec for external form backends" + run: vacuum lint --details ./backend/src/main/resources/com/bakdata/conquery/external/openapi-form-backend.yaml diff --git a/README.md b/README.md index 2390fade0b..b28416c6ae 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ To run the end-to-end test locally: For further informations on this and other tests, please refer to the corresponding [CI configuration](https://github.com/bakdata/conquery/tree/develop/.github/workflows). ### Data Integration + To make you own data (in form of CSVs) availiable to conquery some steps are necessary: 1. Describe your data table structure with meta data, by generating Import- and Table-JSONs. @@ -81,10 +82,16 @@ To make you own data (in form of CSVs) availiable to conquery some steps are nec To get a better impression of the single steps, take a look at the [Tutorials](./tutorial/mimic_iii_demo/README.md) +### Custom Forms + +It is possible to extend the analytic capabilities by adding custom forms. +Custom forms provide an easy way for a user to perform standardized analyses. +For more informations, take a look at the [guide](./docs/custom_forms.md) ## Acknowledgements -This platform was created by [InGef – Institut für angewandte Gesundheitsforschung Berlin GmbH](http://www.ingef.de/) in cooperation with [bakdata GmbH](http://www.bakdata.com) and [Kai Rollmann](https://kairollmann.de/). +This platform was created by [InGef – Institut für angewandte Gesundheitsforschung Berlin GmbH](http://www.ingef.de/) in +cooperation with [bakdata GmbH](http://www.bakdata.com) and [Kai Rollmann](https://kairollmann.de/). [InGef – Institut für angewandte Gesundheitsforschung Berlin GmbH](http://www.ingef.de/)   diff --git a/autodoc/src/main/java/com/bakdata/conquery/Constants.java b/autodoc/src/main/java/com/bakdata/conquery/Constants.java index 36c3fe1fe4..005ad4a0b4 100644 --- a/autodoc/src/main/java/com/bakdata/conquery/Constants.java +++ b/autodoc/src/main/java/com/bakdata/conquery/Constants.java @@ -16,13 +16,13 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; -import com.bakdata.conquery.apiv1.ExecutionStatus; import com.bakdata.conquery.apiv1.FilterTemplate; -import com.bakdata.conquery.apiv1.FullExecutionStatus; import com.bakdata.conquery.apiv1.IdLabel; import com.bakdata.conquery.apiv1.KeyValue; import com.bakdata.conquery.apiv1.MetaDataPatch; -import com.bakdata.conquery.apiv1.OverviewExecutionStatus; +import com.bakdata.conquery.apiv1.execution.ExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.OverviewExecutionStatus; import com.bakdata.conquery.apiv1.frontend.FrontendRoot; import com.bakdata.conquery.apiv1.frontend.FrontendValue; import com.bakdata.conquery.apiv1.query.CQElement; diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/FilterTemplate.java b/backend/src/main/java/com/bakdata/conquery/apiv1/FilterTemplate.java index 7c1aeba082..6a05f227f8 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/FilterTemplate.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/FilterTemplate.java @@ -1,6 +1,6 @@ package com.bakdata.conquery.apiv1; -import java.net.URL; +import java.net.URI; import java.util.List; import javax.validation.constraints.NotEmpty; @@ -10,7 +10,7 @@ import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.io.jackson.serializer.NsIdRef; import com.bakdata.conquery.io.storage.NamespaceStorage; -import com.bakdata.conquery.models.config.SearchConfig; +import com.bakdata.conquery.models.config.IndexConfig; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.datasets.concepts.Searchable; import com.bakdata.conquery.models.identifiable.IdentifiableImpl; @@ -19,6 +19,7 @@ import com.bakdata.conquery.models.index.FrontendValueIndexKey; import com.bakdata.conquery.models.index.IndexService; import com.bakdata.conquery.models.index.search.SearchIndex; +import com.bakdata.conquery.util.io.FileUtil; import com.bakdata.conquery.util.search.TrieSearch; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonCreator; @@ -40,7 +41,7 @@ @ToString @Slf4j @CPSType(id = "CSV_TEMPLATE", base = SearchIndex.class) -public class FilterTemplate extends IdentifiableImpl implements Searchable, SearchIndex { +public class FilterTemplate extends IdentifiableImpl implements Searchable, SearchIndex { private static final long serialVersionUID = 1L; @@ -55,7 +56,7 @@ public class FilterTemplate extends IdentifiableImpl implements S * Path to CSV File. */ @NotNull - private final URL filePath; + private final URI filePath; /** * Value to be sent for filtering. @@ -90,15 +91,18 @@ public boolean isSearchDisabled() { return false; } - public List> getSearches(SearchConfig config, NamespaceStorage storage) { + public List> getSearches(IndexConfig config, NamespaceStorage storage) { + + final URI resolvedURI = FileUtil.getResolvedUri(config.getBaseUrl(), getFilePath()); + log.trace("Resolved filter template reference url for search '{}': {}", this.getId(), resolvedURI); FrontendValueIndex search = indexService.getIndex(new FrontendValueIndexKey( - filePath, + resolvedURI, columnValue, value, optionValue, isGenerateSuffixes() ? getMinSuffixLength() : Integer.MAX_VALUE, - config.getSplit() + config.getSearchSplitChars() )); return List.of(search); diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/QueryProcessor.java b/backend/src/main/java/com/bakdata/conquery/apiv1/QueryProcessor.java index 5d704a8e76..8021c91643 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/QueryProcessor.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/QueryProcessor.java @@ -1,12 +1,14 @@ package com.bakdata.conquery.apiv1; -import java.net.URL; +import java.net.MalformedURLException; +import java.net.URISyntaxException; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; @@ -21,6 +23,10 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; +import com.bakdata.conquery.apiv1.execution.ExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.OverviewExecutionStatus; +import com.bakdata.conquery.apiv1.execution.ResultAsset; import com.bakdata.conquery.apiv1.query.CQElement; import com.bakdata.conquery.apiv1.query.ConceptQuery; import com.bakdata.conquery.apiv1.query.ExternalUpload; @@ -46,6 +52,7 @@ import com.bakdata.conquery.models.config.ColumnConfig; import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.datasets.PreviewConfig; import com.bakdata.conquery.models.datasets.SecondaryIdDescription; import com.bakdata.conquery.models.datasets.concepts.Connector; import com.bakdata.conquery.models.error.ConqueryError; @@ -96,7 +103,7 @@ public ManagedExecution postQuery(Dataset dataset, QueryDescription query, Subje // This maps works as long as we have query visitors that are not configured in anyway. // So adding a visitor twice would replace the previous one but both would have yielded the same result. // For the future a better data structure might be desired that also regards similar QueryVisitors of different configuration - ClassToInstanceMap visitors = MutableClassToInstanceMap.create(); + final ClassToInstanceMap visitors = MutableClassToInstanceMap.create(); query.addVisitors(visitors); // Initialize checks that need to traverse the query tree @@ -184,7 +191,7 @@ private ManagedExecution tryReuse(QueryDescription query, ManagedExecutionId exe execution = newExecution; } - ExecutionState state = execution.getState(); + final ExecutionState state = execution.getState(); if (state.equals(ExecutionState.RUNNING)) { log.trace("The Execution[{}] was already started and its state is: {}", execution.getId(), state); return execution; @@ -200,7 +207,7 @@ private ManagedExecution tryReuse(QueryDescription query, ManagedExecutionId exe public Stream getAllQueries(Dataset dataset, HttpServletRequest req, Subject subject, boolean allProviders) { - Collection allQueries = storage.getAllExecutions(); + final Collection allQueries = storage.getAllExecutions(); return getQueriesFiltered(dataset, RequestAwareUriBuilder.fromRequest(req), subject, allQueries, allProviders); } @@ -217,9 +224,9 @@ public Stream getQueriesFiltered(Dataset datasetId, UriBuilder .filter(q -> q.getState().equals(ExecutionState.DONE) || q.getState().equals(ExecutionState.NEW)) .filter(q -> subject.isPermitted(q, Ability.READ)) .map(mq -> { - OverviewExecutionStatus status = mq.buildStatusOverview(uriBuilder.clone(), subject); + final OverviewExecutionStatus status = mq.buildStatusOverview(uriBuilder.clone(), subject); if (mq.isReadyToDownload()) { - status.setResultUrls(getDownloadUrls(config.getResultProviders(), mq, uriBuilder, allProviders)); + status.setResultUrls(getResultAssets(config.getResultProviders(), mq, uriBuilder, allProviders)); } return status; }); @@ -236,12 +243,21 @@ public Stream getQueriesFiltered(Dataset datasetId, UriBuilder * @param allProviders If true, forces {@link ResultRendererProvider} to return an URL if possible. * @return The modified status */ - public static List getDownloadUrls(List renderer, ManagedExecution exec, UriBuilder uriBuilder, boolean allProviders) { + public static List getResultAssets(List renderer, ManagedExecution exec, UriBuilder uriBuilder, boolean allProviders) { return renderer.stream() - .map(r -> r.generateResultURLs(exec, uriBuilder.clone(), allProviders)) + .map(r -> { + try { + return r.generateResultURLs(exec, uriBuilder.clone(), allProviders); + } + catch (MalformedURLException | URISyntaxException e) { + log.error("Cannot generate result urls for execution '{}' with provider '{}'", exec.getId(), r.getClass().getName()); + return null; + } + }) + .filter(Objects::nonNull) .flatMap(Collection::stream) - .collect(Collectors.toList()); + .toList(); } @@ -305,11 +321,11 @@ public void patchQuery(Subject subject, ManagedExecution execution, MetaDataPatc // TODO remove this, since we don't translate anymore // Patch this query in other datasets - List remainingDatasets = datasetRegistry.getAllDatasets(); + final List remainingDatasets = datasetRegistry.getAllDatasets(); remainingDatasets.remove(execution.getDataset()); for (Dataset dataset : remainingDatasets) { - ManagedExecutionId id = new ManagedExecutionId(dataset.getId(), execution.getQueryId()); + final ManagedExecutionId id = new ManagedExecutionId(dataset.getId(), execution.getQueryId()); final ManagedExecution otherExecution = storage.getExecution(id); if (otherExecution == null) { continue; @@ -349,7 +365,7 @@ public FullExecutionStatus getQueryFullStatus(ManagedExecution query, Subject su final FullExecutionStatus status = query.buildStatusFull(subject); if (query.isReadyToDownload() && subject.isPermitted(query.getDataset(), Ability.DOWNLOAD)) { - status.setResultUrls(getDownloadUrls(config.getResultProviders(), query, url, allProviders)); + status.setResultUrls(getResultAssets(config.getResultProviders(), query, url, allProviders)); } return status; } @@ -402,8 +418,8 @@ public ExternalUploadResult uploadEntities(Subject subject, Dataset dataset, Ext public FullExecutionStatus getSingleEntityExport(Subject subject, UriBuilder uriBuilder, String idKind, String entity, List sources, Dataset dataset, Range dateRange) { final Namespace namespace = datasetRegistry.get(dataset.getId()); - EntityPreviewForm form = - EntityPreviewForm.create(entity, idKind, dateRange, sources, namespace.getPreviewConfig().getSelects()); + final PreviewConfig previewConfig = datasetRegistry.get(dataset.getId()).getPreviewConfig(); + final EntityPreviewForm form = EntityPreviewForm.create(entity, idKind, dateRange, sources, previewConfig.getSelects(), previewConfig.getTimeStratifiedSelects(), datasetRegistry); // TODO make sure that subqueries are also system // TODO do not persist system queries @@ -421,8 +437,8 @@ public FullExecutionStatus getSingleEntityExport(Subject subject, UriBuilder uri } - FullExecutionStatus status = execution.buildStatusFull(subject); - status.setResultUrls(getDownloadUrls(config.getResultProviders(), execution, uriBuilder, false)); + final FullExecutionStatus status = execution.buildStatusFull(subject); + status.setResultUrls(getResultAssets(config.getResultProviders(), execution, uriBuilder, false)); return status; } diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/ExecutionStatus.java b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/ExecutionStatus.java similarity index 92% rename from backend/src/main/java/com/bakdata/conquery/apiv1/ExecutionStatus.java rename to backend/src/main/java/com/bakdata/conquery/apiv1/execution/ExecutionStatus.java index a9c674b11e..4aec080f22 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/ExecutionStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/ExecutionStatus.java @@ -1,4 +1,9 @@ -package com.bakdata.conquery.apiv1; +package com.bakdata.conquery.apiv1.execution; + +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Collections; +import java.util.List; import com.bakdata.conquery.models.execution.ExecutionState; import com.bakdata.conquery.models.identifiable.ids.specific.ManagedExecutionId; @@ -10,12 +15,6 @@ import lombok.ToString; import lombok.experimental.FieldNameConstants; -import java.net.URL; -import java.time.LocalDateTime; -import java.time.ZonedDateTime; -import java.util.Collections; -import java.util.List; - @NoArgsConstructor @ToString @Data @@ -49,7 +48,7 @@ public abstract class ExecutionStatus { /** * The urls under from which the result of the execution can be downloaded as soon as it finished successfully. */ - private List resultUrls = Collections.emptyList(); + private List resultUrls = Collections.emptyList(); } diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/FullExecutionStatus.java b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/FullExecutionStatus.java similarity index 97% rename from backend/src/main/java/com/bakdata/conquery/apiv1/FullExecutionStatus.java rename to backend/src/main/java/com/bakdata/conquery/apiv1/execution/FullExecutionStatus.java index 61c5d4a917..17c058884d 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/FullExecutionStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/FullExecutionStatus.java @@ -1,4 +1,10 @@ -package com.bakdata.conquery.apiv1; +package com.bakdata.conquery.apiv1.execution; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import javax.annotation.Nullable; import com.bakdata.conquery.apiv1.query.QueryDescription; import com.bakdata.conquery.io.jackson.serializer.NsIdRefCollection; @@ -11,11 +17,6 @@ import lombok.NoArgsConstructor; import lombok.experimental.FieldNameConstants; -import javax.annotation.Nullable; -import java.util.Collection; -import java.util.List; -import java.util.Set; - /** * This status holds extensive information about the query description and meta data that is computational heavy * and can produce a larger payload to requests. diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/OverviewExecutionStatus.java b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/OverviewExecutionStatus.java similarity index 83% rename from backend/src/main/java/com/bakdata/conquery/apiv1/OverviewExecutionStatus.java rename to backend/src/main/java/com/bakdata/conquery/apiv1/execution/OverviewExecutionStatus.java index 3e06d0e74c..f68c83ec96 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/OverviewExecutionStatus.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/OverviewExecutionStatus.java @@ -1,4 +1,4 @@ -package com.bakdata.conquery.apiv1; +package com.bakdata.conquery.apiv1.execution; import lombok.NoArgsConstructor; diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/execution/ResultAsset.java b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/ResultAsset.java new file mode 100644 index 0000000000..4e6e9f2d3e --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/execution/ResultAsset.java @@ -0,0 +1,31 @@ +package com.bakdata.conquery.apiv1.execution; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.dropwizard.validation.ValidationMethod; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public record ResultAsset(String label, URI url) { + + @JsonIgnore + @ValidationMethod(message = "Generated assetId was blank") + public boolean isAssetIdValid() { + return StringUtils.isBlank(getAssetId()); + } + + /** + * An url-encoded id of this asset. + * It derives from the last path section of the file part in this url. + */ + @JsonIgnore + public String getAssetId() { + + final String path = url.getPath(); + return URLEncoder.encode(path.substring(path.lastIndexOf('/') + 1), StandardCharsets.UTF_8); + } +} diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java new file mode 100644 index 0000000000..0a8a8e6908 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/ExternalForm.java @@ -0,0 +1,165 @@ +package com.bakdata.conquery.apiv1.forms; + + +import static com.bakdata.conquery.apiv1.forms.ExternalForm.Deserializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; + +import com.bakdata.conquery.apiv1.query.QueryDescription; +import com.bakdata.conquery.io.cps.CPSType; +import com.bakdata.conquery.io.cps.CPSTypeIdResolver; +import com.bakdata.conquery.io.cps.SubTyped; +import com.bakdata.conquery.io.storage.MetaStorage; +import com.bakdata.conquery.models.auth.entities.User; +import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.execution.ManagedExecution; +import com.bakdata.conquery.models.forms.frontendconfiguration.FormScanner; +import com.bakdata.conquery.models.forms.frontendconfiguration.FormType; +import com.bakdata.conquery.models.forms.managed.ExternalExecution; +import com.bakdata.conquery.models.i18n.I18n; +import com.bakdata.conquery.models.identifiable.ids.specific.ManagedExecutionId; +import com.bakdata.conquery.models.query.QueryResolveContext; +import com.bakdata.conquery.models.query.Visitable; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.common.base.Strings; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Setter +@CPSType(id = "EXTERNAL_FORM", base = QueryDescription.class, subTyped = true) +@JsonDeserialize(using = Deserializer.class) +@RequiredArgsConstructor +@Slf4j +public class ExternalForm extends Form implements SubTyped { + + + @JsonValue + private final ObjectNode node; + + private final String subType; + + @Override + public String getFormType() { + return CPSTypeIdResolver.createSubTyped(this.getClass().getAnnotation(CPSType.class).id(), getSubType()); + } + + @Override + public String getLocalizedTypeLabel() { + final JsonNode formTitle = node.get("title"); + if (formTitle != null && formTitle.isTextual()) { + return formTitle.asText(); + } + + // Form had no specific title set. Try localized lookup in FormConfig + Locale preferredLocale = I18n.LOCALE.get(); + FormType frontendConfig = FormScanner.FRONTEND_FORM_CONFIGS.get(this.getFormType()); + + if (frontendConfig == null) { + return getSubType(); + } + + JsonNode titleObj = frontendConfig.getRawConfig().path("title"); + if (!titleObj.isObject()) { + log.trace("Expected \"title\" member to be of type Object in {}", frontendConfig); + return getSubType(); + } + + List localesFound = new ArrayList<>(); + titleObj.fieldNames().forEachRemaining((lang) -> localesFound.add(new Locale(lang))); + if (localesFound.isEmpty()) { + log.trace("Could not extract a locale from the provided FrontendConfig: {}", frontendConfig); + return getSubType(); + } + Locale chosenLocale = Locale.lookup(Locale.LanguageRange.parse(preferredLocale.getLanguage()), localesFound); + if (chosenLocale == null) { + chosenLocale = localesFound.get(0); + log.trace("Locale lookup did not return a matching locale. Using the first title encountered: {}", chosenLocale); + } + JsonNode title = titleObj.path(chosenLocale.getLanguage()); + if (!title.isTextual()) { + log.trace("Expected a textual node for the localized title. Was: {}", title.getNodeType()); + return getSubType(); + } + String ret = title.asText(); + + log.trace("Extracted localized title. Was: \"{}\"", ret); + return ret.isBlank() ? getSubType() : ret; + } + + @Override + public ManagedExecution toManagedExecution(User user, Dataset submittedDataset, MetaStorage storage) { + return new ExternalExecution(this, user, submittedDataset, storage); + } + + @Override + public Set collectRequiredQueries() { + return Collections.emptySet(); + } + + @Override + public void resolve(QueryResolveContext context) { + + } + + @Override + public void visit(Consumer visitor) { + + } + + + /** + * Custom deserializer, that picks up the sub-typing information extracted by + * the {@link CPSTypeIdResolver}. This means also that this deserializer + * currently can only deserialize secondary types (which is when the property + * type of this form is not {@link ExternalForm} but a type higher in the class + * hierarchy) + */ + @NoArgsConstructor + @AllArgsConstructor + public static class Deserializer extends JsonDeserializer implements ContextualDeserializer { + + private String subTypeId; + + @Override + public ExternalForm deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + + if (Strings.isNullOrEmpty(subTypeId)) { + throw new IllegalStateException("This class needs subtype information for deserialization."); + } + ObjectNode tree = p.readValueAsTree(); + return new ExternalForm(tree, subTypeId); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { + // This is only called once per typeId@SubTypeId + String subTypeId = (String) ctxt.getAttribute(CPSTypeIdResolver.ATTRIBUTE_SUB_TYPE); + if (Strings.isNullOrEmpty(subTypeId)) { + throw new IllegalStateException("This class needs subtype information for deserialization."); + } + return new Deserializer(subTypeId); + } + + } +} diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java index 783015148c..451c39d333 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/Form.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ClassToInstanceMap; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -21,6 +22,7 @@ /** * API representation of a form query. */ +@EqualsAndHashCode public abstract class Form implements QueryDescription { /** @@ -30,6 +32,7 @@ public abstract class Form implements QueryDescription { @Nullable @Getter @Setter + @EqualsAndHashCode.Exclude private JsonNode values; @JsonIgnore diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/AbsoluteMode.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/AbsoluteMode.java index 1b7e00d556..c71dbd788c 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/AbsoluteMode.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/AbsoluteMode.java @@ -18,12 +18,14 @@ import com.bakdata.conquery.models.query.QueryResolveContext; import com.bakdata.conquery.models.query.Visitable; import com.fasterxml.jackson.annotation.JsonView; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @Getter @Setter @CPSType(id = "ABSOLUTE", base = Mode.class) +@EqualsAndHashCode(callSuper = true, doNotUseGetters = true) public class AbsoluteMode extends Mode { @NotNull @Valid @@ -35,6 +37,7 @@ public class AbsoluteMode extends Mode { @JsonView(View.InternalCommunication.class) + @EqualsAndHashCode.Exclude private ArrayConceptQuery resolvedFeatures; @Override diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/EntityDateMode.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/EntityDateMode.java index 619c5668db..a7223e4e69 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/EntityDateMode.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/EntityDateMode.java @@ -19,6 +19,7 @@ import com.bakdata.conquery.models.query.QueryResolveContext; import com.bakdata.conquery.models.query.Visitable; import com.fasterxml.jackson.annotation.JsonView; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -43,6 +44,7 @@ public void visit(Consumer visitor) { @JsonView(View.InternalCommunication.class) + @EqualsAndHashCode.Exclude private ArrayConceptQuery resolvedFeatures; @Override diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/ExportForm.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/ExportForm.java index 7021a7d74e..381e4bf70d 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/ExportForm.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/ExportForm.java @@ -39,6 +39,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -46,6 +47,7 @@ @Getter @Setter @CPSType(id = "EXPORT_FORM", base = QueryDescription.class) +@EqualsAndHashCode(callSuper = true) public class ExportForm extends Form implements InternalForm { @NotNull @@ -53,6 +55,7 @@ public class ExportForm extends Form implements InternalForm { private ManagedExecutionId queryGroupId; @JsonIgnore + @EqualsAndHashCode.Exclude private ManagedQuery queryGroup; @NotNull @@ -61,6 +64,7 @@ public class ExportForm extends Form implements InternalForm { private Mode timeMode; @NotEmpty + @Valid private List features = ImmutableList.of(); @NotNull @@ -70,8 +74,10 @@ public class ExportForm extends Form implements InternalForm { private boolean alsoCreateCoarserSubdivisions = false; @JsonIgnore + @EqualsAndHashCode.Exclude private Query prerequisite; @JsonIgnore + @EqualsAndHashCode.Exclude private List resolvedResolutions; @Override public void visit(Consumer visitor) { diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/Mode.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/Mode.java index c44b6d525a..56243c6db2 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/Mode.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/Mode.java @@ -6,16 +6,19 @@ import com.bakdata.conquery.models.query.Visitable; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, include = JsonTypeInfo.As.PROPERTY, property = "value") @CPSBase +@EqualsAndHashCode public abstract class Mode implements Visitable { @Getter @Setter @JsonBackReference + @EqualsAndHashCode.Exclude private ExportForm form; public abstract void resolve(QueryResolveContext context); diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/RelativeMode.java b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/RelativeMode.java index 9c4d0f24e6..07c5007341 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/RelativeMode.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/forms/export_form/RelativeMode.java @@ -17,11 +17,13 @@ import com.bakdata.conquery.models.query.QueryResolveContext; import com.bakdata.conquery.models.query.Visitable; import com.fasterxml.jackson.annotation.JsonView; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @Getter @Setter -@CPSType(id="RELATIVE", base=Mode.class) +@CPSType(id = "RELATIVE", base = Mode.class) +@EqualsAndHashCode(callSuper = true) public class RelativeMode extends Mode { @NotNull private CalendarUnit timeUnit; @@ -36,6 +38,7 @@ public class RelativeMode extends Mode { @JsonView(View.InternalCommunication.class) + @EqualsAndHashCode.Exclude private ArrayConceptQuery resolvedFeatures; @Override diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java index 9769140747..3078cf6fc4 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendPreviewConfig.java @@ -5,6 +5,7 @@ import com.bakdata.conquery.io.jackson.serializer.NsIdRef; import com.bakdata.conquery.io.jackson.serializer.NsIdRefCollection; +import com.bakdata.conquery.models.common.daterange.CDateRange; import com.bakdata.conquery.models.datasets.concepts.Concept; import com.bakdata.conquery.models.datasets.concepts.filters.Filter; import com.fasterxml.jackson.annotation.JsonProperty; @@ -18,6 +19,8 @@ public static class Labelled { private final String label; } + private final CDateRange observationPeriodDefault; + private final Collection all; @JsonProperty("default") private final Collection defaultConnectors; diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendValidityDate.java b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendValidityDate.java index d620185bb2..99772d402b 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendValidityDate.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/frontend/FrontendValidityDate.java @@ -2,12 +2,21 @@ import java.util.List; +import javax.annotation.Nullable; + import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class FrontendValidityDate { + + /** + * Further information about the available validity dates. + */ + @Nullable + private String tooltip; + private String defaultValue; private List options; } diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/CQElement.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/CQElement.java index 6f4a9fe289..704e672cb8 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/CQElement.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/CQElement.java @@ -23,11 +23,13 @@ import com.bakdata.conquery.models.query.resultinfo.ResultInfo; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") @CPSBase +@EqualsAndHashCode public abstract class CQElement implements Visitable { /** diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java index b49088bb03..6fc57971a6 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/TableExportQuery.java @@ -19,7 +19,7 @@ import javax.validation.constraints.NotNull; import com.bakdata.conquery.ConqueryConstants; -import com.bakdata.conquery.apiv1.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; import com.bakdata.conquery.apiv1.query.concept.filter.CQTable; import com.bakdata.conquery.apiv1.query.concept.specific.CQConcept; import com.bakdata.conquery.io.cps.CPSType; diff --git a/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/CQTable.java b/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/CQTable.java index 52a17dd036..bea2268426 100644 --- a/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/CQTable.java +++ b/backend/src/main/java/com/bakdata/conquery/apiv1/query/concept/filter/CQTable.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.validation.ValidationMethod; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -25,6 +26,7 @@ @Getter @Setter @ToString(exclude = "concept") +@EqualsAndHashCode public class CQTable { @Valid @NotNull @@ -35,6 +37,7 @@ public class CQTable { private List getSelects() { return getInfoCardSelects().stream() - .map(InfoCardSelect::getSelect) + .map(InfoCardSelect::select) .map(id -> datasetRegistry.findRegistry(id.getDataset()).getOptional(id)) .flatMap(Optional::stream) .collect(Collectors.toList()); @@ -171,23 +214,6 @@ public Concept resolveSearchConcept() { .collect(MoreCollectors.onlyElement()); } - @Data - public static class InfoCardSelect { - /** - * User facing label of the select. - */ - private final String label; - /** - * Id (without dataset) of the select. - */ - private final SelectId select; - - @JsonCreator - public InfoCardSelect(String label, SelectId select) { - this.label = label; - this.select = select; - } - } } diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/Table.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/Table.java index a568edfaac..856fdd54b7 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/Table.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/Table.java @@ -38,9 +38,9 @@ public class Table extends Labeled implements NamespacedIdentifiable secondaryIds = new HashSet<>(); + final Set secondaryIds = new HashSet<>(); for (Column column : columns) { - SecondaryIdDescription secondaryId = column.getSecondaryId(); + final SecondaryIdDescription secondaryId = column.getSecondaryId(); if (secondaryId != null && !secondaryIds.add(secondaryId)) { log.error("{} is duplicated", secondaryId); return false; @@ -49,16 +49,27 @@ public boolean isDistinctSecondaryIds() { return true; } + @ValidationMethod(message = "Column labels must be unique.") + @JsonIgnore + public boolean isDistinctLabels() { + final Set labels = new HashSet<>(); + + for (Column column : columns) { + if (!labels.add(column.getLabel())) { + log.error("Label `{}` for `{}` is duplicated", column.getLabel(), column.getId()); + return false; + } + } + return true; + } + @Override public TableId createId() { return new TableId(dataset.getId(), getName()); } public Stream findImports(NamespacedStorage storage) { - return storage - .getAllImports() - .stream() - .filter(imp -> imp.getTable().equals(this)); + return storage.getAllImports().stream().filter(imp -> imp.getTable().equals(this)); } public Column getColumnByName(@NotNull String columnName) { diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Concept.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Concept.java index ff14df488b..e18fcd195c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Concept.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Concept.java @@ -14,6 +14,7 @@ import com.bakdata.conquery.models.auth.permissions.ConceptPermission; import com.bakdata.conquery.models.auth.permissions.ConqueryPermission; import com.bakdata.conquery.models.common.CDateSet; +import com.bakdata.conquery.models.datasets.Column; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.datasets.concepts.select.Select; import com.bakdata.conquery.models.exceptions.ConfigurationException; @@ -25,9 +26,11 @@ import com.bakdata.conquery.models.query.queryplan.filter.FilterNode; import com.bakdata.conquery.models.query.queryplan.specific.FiltersNode; import com.bakdata.conquery.models.query.queryplan.specific.Leaf; +import com.bakdata.conquery.models.query.queryplan.specific.ValidityDateNode; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonManagedReference; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -40,6 +43,7 @@ @ToString(of = {"connectors"}) @Getter @Setter +@EqualsAndHashCode(callSuper = true) public abstract class Concept extends ConceptElement implements Authorized { /** @@ -86,11 +90,12 @@ public int countElements() { /** * Allows concepts to create their own altered FiltersNode if necessary. */ - public QPNode createConceptQuery(QueryPlanContext context, List> filters, List> aggregators, List> eventDateAggregators) { - if (filters.isEmpty() && aggregators.isEmpty()) { - return new Leaf(); - } - return FiltersNode.create(filters, aggregators, eventDateAggregators); + public QPNode createConceptQuery(QueryPlanContext context, List> filters, List> aggregators, List> eventDateAggregators, Column validityDateColumn) { + QPNode child = filters.isEmpty() && aggregators.isEmpty() ? new Leaf() : FiltersNode.create(filters, aggregators, eventDateAggregators); + + + // Only if a validityDateColumn exists, capsule children in ValidityDateNode + return validityDateColumn != null ? new ValidityDateNode(validityDateColumn, child) : child; } @Override diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ConceptElement.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ConceptElement.java index 77da68d313..68efca1c56 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ConceptElement.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ConceptElement.java @@ -12,9 +12,11 @@ import com.bakdata.conquery.models.identifiable.ids.specific.ConceptElementId; import com.bakdata.conquery.models.identifiable.ids.specific.ConceptTreeChildId; import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +@EqualsAndHashCode(callSuper = true) public abstract class ConceptElement>> extends Labeled implements NamespacedIdentifiable { @Getter @@ -38,12 +40,12 @@ public abstract class ConceptElement getConcept(); diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Connector.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Connector.java index d76bc544aa..90ae303a4b 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Connector.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Connector.java @@ -7,6 +7,7 @@ import java.util.stream.Collectors; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import javax.validation.Valid; import javax.validation.constraints.NotNull; @@ -20,6 +21,7 @@ import com.bakdata.conquery.models.identifiable.ids.NamespacedIdentifiable; import com.bakdata.conquery.models.identifiable.ids.specific.ConnectorId; import com.bakdata.conquery.models.identifiable.ids.specific.FilterId; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -29,6 +31,7 @@ import com.google.common.collect.Multiset.Entry; import io.dropwizard.validation.ValidationMethod; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -46,12 +49,17 @@ public abstract class Connector extends Labeled implements SelectHo public static final int[] NOT_CONTAINED = new int[]{-1}; private static final long serialVersionUID = 1L; + @Nullable + @JsonAlias("validityDatesTooltip") + private String validityDatesDescription; + @NotNull @JsonManagedReference @Valid private List validityDates = new ArrayList<>(); @JsonBackReference + @EqualsAndHashCode.Exclude private Concept concept; @JsonIgnore diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java index 85cf6322b8..edbf99b1c3 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/FrontEndConceptBuilder.java @@ -224,6 +224,7 @@ public static FrontendTable createTable(Connector con) { if(con.getValidityDates().size() > 1) { result.setDateColumn( new FrontendValidityDate( + con.getValidityDatesDescription(), null, con .getValidityDates() diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Searchable.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Searchable.java index acae609303..1047c74a4c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Searchable.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/Searchable.java @@ -6,7 +6,10 @@ import com.bakdata.conquery.apiv1.frontend.FrontendValue; import com.bakdata.conquery.io.storage.NamespaceStorage; -import com.bakdata.conquery.models.config.SearchConfig; +import com.bakdata.conquery.models.config.IndexConfig; +import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.identifiable.Identifiable; +import com.bakdata.conquery.models.identifiable.ids.Id; import com.bakdata.conquery.models.query.FilterSearch; import com.bakdata.conquery.util.search.TrieSearch; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -16,18 +19,22 @@ *

* Searchable classes describe how a search should be constructed, and provide the values with getSearchValues. */ -public interface Searchable { +public interface Searchable>> extends Identifiable { + + public Dataset getDataset(); + /** * All available {@link FrontendValue}s for searching in a {@link TrieSearch}. */ - List> getSearches(SearchConfig config, NamespaceStorage storage); + List> getSearches(IndexConfig config, NamespaceStorage storage); /** * The actual Searchables to use, if there is potential for deduplication/pooling. + * * @implSpec The order of objects returned is used to also sort search results from different sources. */ @JsonIgnore - default List getSearchReferences() { + default List> getSearchReferences() { //Hopefully the only candidate will be Column return List.of(this); } diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/StructureNode.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/StructureNode.java index 9b96172fe8..42c74d823d 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/StructureNode.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/StructureNode.java @@ -7,15 +7,15 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; -import com.bakdata.conquery.io.jackson.serializer.NsIdRef; import com.bakdata.conquery.apiv1.KeyValue; +import com.bakdata.conquery.io.jackson.serializer.NsIdRef; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.identifiable.Labeled; import com.bakdata.conquery.models.identifiable.ids.specific.ConceptId; import com.bakdata.conquery.models.identifiable.ids.specific.StructureNodeId; import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonManagedReference; - +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.ToString; @@ -32,6 +32,7 @@ public class StructureNode extends Labeled { @Valid @JsonManagedReference(MANAGED_STRUCTURE_STRUCTURE) private List children = Collections.emptyList(); @JsonBackReference(MANAGED_STRUCTURE_STRUCTURE) + @EqualsAndHashCode.Exclude private StructureNode parent; @Getter private LinkedHashSet containedRoots = new LinkedHashSet<>(); diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ValidityDate.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ValidityDate.java index a827c86e47..5bdbe1c8bf 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ValidityDate.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/ValidityDate.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import io.dropwizard.validation.ValidationMethod; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -26,6 +27,7 @@ public class ValidityDate extends Labeled implements NamespacedI @NotNull private Column column; @JsonBackReference + @EqualsAndHashCode.Exclude private Connector connector; @Override diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/Filter.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/Filter.java index 98097bc40c..4447e29f9f 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/Filter.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/Filter.java @@ -17,6 +17,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.dropwizard.validation.ValidationMethod; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -31,12 +32,14 @@ @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") @CPSBase @Slf4j +@EqualsAndHashCode(callSuper = true) public abstract class Filter extends Labeled implements NamespacedIdentifiable { private String unit; @JsonAlias("description") private String tooltip; @JsonBackReference + @EqualsAndHashCode.Exclude private Connector connector; private String pattern; private Boolean allowDropFile; diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/specific/SelectFilter.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/specific/SelectFilter.java index c564d8cb2a..e8f06ecc24 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/specific/SelectFilter.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/filters/specific/SelectFilter.java @@ -12,11 +12,12 @@ import com.bakdata.conquery.io.jackson.View; import com.bakdata.conquery.io.jackson.serializer.NsIdRef; import com.bakdata.conquery.io.storage.NamespaceStorage; -import com.bakdata.conquery.models.config.SearchConfig; +import com.bakdata.conquery.models.config.IndexConfig; import com.bakdata.conquery.models.datasets.concepts.Searchable; import com.bakdata.conquery.models.datasets.concepts.filters.SingleColumnFilter; import com.bakdata.conquery.models.events.MajorTypeId; import com.bakdata.conquery.models.exceptions.ConceptConfigurationException; +import com.bakdata.conquery.models.identifiable.ids.specific.FilterId; import com.bakdata.conquery.models.query.FilterSearch; import com.bakdata.conquery.util.search.TrieSearch; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -35,7 +36,7 @@ @NoArgsConstructor @Slf4j @JsonIgnoreProperties({"searchType"}) -public abstract class SelectFilter extends SingleColumnFilter implements Searchable { +public abstract class SelectFilter extends SingleColumnFilter implements Searchable { /** * user given mapping from the values in the CSVs to shown labels @@ -88,8 +89,8 @@ public boolean isNotUsingTemplateAndLabels() { private boolean generateSearchSuffixes = true; @Override - public List getSearchReferences() { - final List out = new ArrayList<>(); + public List> getSearchReferences() { + final List> out = new ArrayList<>(); if (getTemplate() != null) { out.add(getTemplate()); @@ -126,9 +127,9 @@ public boolean isSearchDisabled() { } @Override - public List> getSearches(SearchConfig config, NamespaceStorage storage) { + public List> getSearches(IndexConfig config, NamespaceStorage storage) { - TrieSearch search = new TrieSearch<>(config.getSuffixLength(), config.getSplit()); + TrieSearch search = new TrieSearch<>(config.getSearchSuffixLength(), config.getSearchSplitChars()); labels.entrySet() .stream() .map(entry -> new FrontendValue(entry.getKey(), entry.getValue())) diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/select/Select.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/select/Select.java index b66174f619..182d13ab59 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/select/Select.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/select/Select.java @@ -22,17 +22,22 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.dropwizard.validation.ValidationMethod; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; -@JsonTypeInfo(use=JsonTypeInfo.Id.CUSTOM, property="type") +@JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") @CPSBase @Slf4j +@EqualsAndHashCode(callSuper = true) public abstract class Select extends Labeled implements NamespacedIdentifiable { - @JsonBackReference @Getter @Setter + @EqualsAndHashCode.Exclude + @JsonBackReference + @Getter + @Setter private SelectHolder holder; @JsonIgnore @@ -41,7 +46,8 @@ public Dataset getDataset() { return getHolder().findConcept().getDataset(); } - @Setter @Getter + @Setter + @Getter private String description; /** diff --git a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/tree/ConceptTreeChild.java b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/tree/ConceptTreeChild.java index 829eed8120..7e941a607d 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/tree/ConceptTreeChild.java +++ b/backend/src/main/java/com/bakdata/conquery/models/datasets/concepts/tree/ConceptTreeChild.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.annotation.JsonBackReference; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonManagedReference; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -32,6 +33,7 @@ public class ConceptTreeChild extends ConceptElement impleme @JsonBackReference @Getter @Setter + @EqualsAndHashCode.Exclude private ConceptTreeNode parent; @JsonIgnore @Getter diff --git a/backend/src/main/java/com/bakdata/conquery/models/execution/InternalExecution.java b/backend/src/main/java/com/bakdata/conquery/models/execution/InternalExecution.java index 73c02d0ddb..f34aa3223f 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/execution/InternalExecution.java +++ b/backend/src/main/java/com/bakdata/conquery/models/execution/InternalExecution.java @@ -20,4 +20,5 @@ public interface InternalExecution { * Is called once per shard node */ void addResult(R result); + } diff --git a/backend/src/main/java/com/bakdata/conquery/models/execution/ManagedExecution.java b/backend/src/main/java/com/bakdata/conquery/models/execution/ManagedExecution.java index e161f9f663..047ecc25eb 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/execution/ManagedExecution.java +++ b/backend/src/main/java/com/bakdata/conquery/models/execution/ManagedExecution.java @@ -16,9 +16,9 @@ import javax.validation.constraints.NotNull; import javax.ws.rs.core.UriBuilder; -import com.bakdata.conquery.apiv1.ExecutionStatus; -import com.bakdata.conquery.apiv1.FullExecutionStatus; -import com.bakdata.conquery.apiv1.OverviewExecutionStatus; +import com.bakdata.conquery.apiv1.execution.ExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.OverviewExecutionStatus; import com.bakdata.conquery.apiv1.query.QueryDescription; import com.bakdata.conquery.io.cps.CPSBase; import com.bakdata.conquery.io.jackson.serializer.MetaIdRef; @@ -52,6 +52,7 @@ import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Uninterruptibles; import lombok.AccessLevel; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.Setter; @@ -66,6 +67,7 @@ @Slf4j @CPSBase @JsonTypeInfo(use = JsonTypeInfo.Id.CUSTOM, property = "type") +@EqualsAndHashCode(callSuper = false) public abstract class ManagedExecution extends IdentifiableImpl implements Taggable, Shareable, Labelable, Owned, Visitable { /** @@ -94,28 +96,38 @@ public abstract class ManagedExecution extends IdentifiableImpl implements Sharea @NotNull private String[] tags = ArrayUtils.EMPTY_STRING_ARRAY; private boolean shared = false; - + /** - * This is a blackbox for us at the moment, where the front end saves the state of the - * formular, when the user saved it. + * This is a blackbox for us at the moment, where the front end saves the state of the + * form, when the user saved it. */ @NotNull private JsonNode values; diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQuery.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQuery.java index b624e7dc1d..6ce35c51de 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQuery.java @@ -32,6 +32,16 @@ @CPSType(id = "ABSOLUTE_FORM_QUERY", base = QueryDescription.class) public class AbsoluteFormQuery extends Query { + /** + * Index of the column, where the Resolutions name will be placed. + */ + public static final int RESOLUTION_INDEX = 0; + + /** + * Indef of the column, where the time periods will be placed. + */ + public static final int TIME_INDEX = 2; + /** * see {@linkplain this#getResultInfos()}. */ diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQueryPlan.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQueryPlan.java index af51219d58..4224061a7b 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQueryPlan.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/AbsoluteFormQueryPlan.java @@ -11,8 +11,10 @@ import com.bakdata.conquery.models.query.results.MultilineEntityResult; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; @Getter @RequiredArgsConstructor +@ToString public class AbsoluteFormQueryPlan implements QueryPlan { private final QueryPlan query; diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/EntityDateQueryPlan.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/EntityDateQueryPlan.java index 0464b9c2a3..77218e9cdf 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/EntityDateQueryPlan.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/EntityDateQueryPlan.java @@ -17,11 +17,13 @@ import com.bakdata.conquery.models.query.results.EntityResult; import com.bakdata.conquery.models.query.results.MultilineEntityResult; import lombok.RequiredArgsConstructor; +import lombok.ToString; /** * Implementation of the QueryPlan for an {@link EntityDateQuery}. */ @RequiredArgsConstructor +@ToString public class EntityDateQueryPlan implements QueryPlan { diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ExternalExecution.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ExternalExecution.java new file mode 100644 index 0000000000..ef6f840542 --- /dev/null +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ExternalExecution.java @@ -0,0 +1,191 @@ +package com.bakdata.conquery.models.forms.managed; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import javax.ws.rs.core.Response; + +import com.bakdata.conquery.apiv1.execution.ExecutionStatus; +import com.bakdata.conquery.apiv1.execution.ResultAsset; +import com.bakdata.conquery.apiv1.forms.ExternalForm; +import com.bakdata.conquery.io.cps.CPSType; +import com.bakdata.conquery.io.external.form.ExternalFormBackendApi; +import com.bakdata.conquery.io.external.form.ExternalTaskState; +import com.bakdata.conquery.io.result.ExternalResult; +import com.bakdata.conquery.io.storage.MetaStorage; +import com.bakdata.conquery.models.auth.entities.Subject; +import com.bakdata.conquery.models.auth.entities.User; +import com.bakdata.conquery.models.config.FormBackendConfig; +import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.error.ConqueryError; +import com.bakdata.conquery.models.execution.ExecutionState; +import com.bakdata.conquery.models.execution.ManagedExecution; +import com.bakdata.conquery.resources.api.ResultExternalResource; +import com.bakdata.conquery.util.AuthUtil; +import com.fasterxml.jackson.annotation.JacksonInject; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.OptBoolean; +import com.google.common.base.Preconditions; +import com.google.common.collect.MoreCollectors; +import it.unimi.dsi.fastutil.Pair; +import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +/** + * This execution type is for external form backends which use Conquery as a proxy for their task execution. + * An {@link ExternalForm} is wrapped in this execution to keep + * track of it's state using a configured API (see {@link FormBackendConfig} and {@link ExternalFormBackendApi}). + */ +@Slf4j +@CPSType(id = "EXTERNAL_EXECUTION", base = ManagedExecution.class) +@EqualsAndHashCode(callSuper = true, doNotUseGetters = true) +public class ExternalExecution extends ManagedForm implements ExternalResult { + + + private UUID externalTaskId; + + @JsonIgnore + @EqualsAndHashCode.Exclude + private ExternalFormBackendApi api; + + @JsonIgnore + @EqualsAndHashCode.Exclude + private FormBackendConfig formBackendConfig; + + @JsonIgnore + @EqualsAndHashCode.Exclude + private User serviceUser; + + + /** + * Pairs of external result assets (internal url) and their internal asset builder. + * The internal asset builder generates the asset url with the context of a user request. + */ + @JsonIgnore + private List> resultsAssetMap = Collections.emptyList(); + + @JsonCreator + protected ExternalExecution(@JacksonInject(useInput = OptBoolean.FALSE) MetaStorage storage) { + super(storage); + } + + public ExternalExecution(ExternalForm form, User user, Dataset dataset, MetaStorage storage) { + super(form, user, dataset, storage); + } + + @Override + protected void doInitExecutable() { + formBackendConfig = getConfig().getPluginConfigs(FormBackendConfig.class) + .filter(c -> c.supportsFormType(getSubmittedForm().getFormType())) + .collect(MoreCollectors.onlyElement()); + + api = formBackendConfig.createApi(); + } + + @Override + public void start() { + + synchronized (this) { + + if (externalTaskId != null) { + syncExternalState(); + } + + if (getState() == ExecutionState.RUNNING) { + throw new ConqueryError.ExecutionProcessingError(); + } + + super.start(); + + // Create service user + serviceUser = formBackendConfig.createServiceUser(getOwner(), getDataset()); + + final ExternalTaskState externalTaskState = api.postForm(getSubmitted(), getOwner(), serviceUser, getDataset()); + + externalTaskId = externalTaskState.getId(); + } + } + + private synchronized void syncExternalState() { + Preconditions.checkNotNull(externalTaskId, "Cannot check external task, because no Id is present"); + + final ExternalTaskState formState = api.getFormState(externalTaskId); + + switch (formState.getStatus()) { + + case RUNNING -> { + setState(ExecutionState.RUNNING); + setProgress(formState.getProgress().floatValue()); + } + case FAILURE -> fail(formState.getError()); + case SUCCESS -> { + resultsAssetMap = registerResultAssets(formState); + finish(ExecutionState.DONE); + } + } + } + + private List> registerResultAssets(ExternalTaskState response) { + final List> assetMap = new ArrayList<>(); + response.getResults().forEach(asset -> + { + assetMap.add(Pair.of(asset, createResultAssetBuilder(asset))); + }); + return assetMap; + } + + /** + * The {@link ResultAsset} is request-dependent, so we can prepare only builder here which takes an url builder. + */ + private AssetBuilder createResultAssetBuilder(ResultAsset asset) { + return (uriBuilder) -> { + try { + final URI externalDownloadURL = ResultExternalResource.getDownloadURL(uriBuilder.clone(), this, asset.getAssetId()); + return new ResultAsset(asset.label(), externalDownloadURL); + } + catch (URISyntaxException e) { + throw new RuntimeException(e); + } + }; + } + + @Override + public void setStatusBase(@NonNull Subject subject, @NonNull ExecutionStatus status) { + syncExternalState(); + + super.setStatusBase(subject, status); + } + + @Override + public Stream getResultAssets() { + return resultsAssetMap.stream().map(Pair::value); + } + + @Override + public Response fetchExternalResult(String assetId) { + final ResultAsset resultRef = resultsAssetMap.stream() + .map(Pair::key).filter(a -> a.getAssetId().equals(assetId)) + .collect(MoreCollectors.onlyElement()); + + return api.getResult(resultRef.url()); + } + + @Override + protected void finish(ExecutionState executionState) { + if (getState().equals(executionState)) { + return; + } + super.finish(executionState); + synchronized (this) { + AuthUtil.cleanUpUserAndBelongings(serviceUser, getStorage()); + serviceUser = null; + } + } +} diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/FormQueryPlan.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/FormQueryPlan.java index 321b70e805..3b5df25c50 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/FormQueryPlan.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/FormQueryPlan.java @@ -5,7 +5,6 @@ import java.util.Optional; import java.util.OptionalInt; -import com.bakdata.conquery.apiv1.forms.FeatureGroup; import com.bakdata.conquery.models.common.CDateSet; import com.bakdata.conquery.models.forms.util.DateContext; import com.bakdata.conquery.models.forms.util.Resolution; @@ -19,8 +18,12 @@ import com.bakdata.conquery.models.query.results.SinglelineEntityResult; import com.bakdata.conquery.util.QueryUtils; import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; @Getter +@ToString +@Slf4j public class FormQueryPlan implements QueryPlan { private final List dateContexts; @@ -37,7 +40,8 @@ public FormQueryPlan(List dateContexts, ArrayConceptQueryPlan featu if (dateContexts.size() <= 0) { - // There is nothing to do for this FormQueryPlan but we will return an empty result when its executed + // There is nothing to do for this FormQueryPlan, but we will return an empty result when its executed + log.warn("dateContexts are empty. Will not produce a result."); constantCount = 3; withRelativeEventDate = false; return; diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedForm.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedForm.java index 63b8001e0b..410460a5ac 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedForm.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedForm.java @@ -4,7 +4,6 @@ import com.bakdata.conquery.apiv1.forms.Form; import com.bakdata.conquery.apiv1.forms.FormConfigAPI; -import com.bakdata.conquery.apiv1.query.QueryDescription; import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.io.storage.MetaStorage; import com.bakdata.conquery.models.auth.entities.User; @@ -16,41 +15,44 @@ import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.OptBoolean; +import com.fasterxml.jackson.databind.DatabindContext; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; /** * Internal runtime representation of a form query. */ -@Getter -@Setter @ToString @Slf4j @EqualsAndHashCode(callSuper = true) @CPSType(id = "MANAGED_FORM", base = ManagedExecution.class) -public class ManagedForm extends ManagedExecution { +public abstract class ManagedForm extends ManagedExecution { /** - * The form that was submitted through the api. + * The submitted form for this execution. + * + * @implNote We use the type {@link Form} here rather than the type parameter F. + * Using F causes the class to have a concrete type at runtime which in turn skips + * the object inspection of Jackson to look at the actual type member of the object (see {@link com.bakdata.conquery.io.cps.CPSTypeIdResolver#typeFromId(DatabindContext, String)}). + * This causes a problem, when the object uses types with {@link com.bakdata.conquery.io.cps.SubTyped}, + * as the subtype is only added to the {@link com.fasterxml.jackson.databind.DeserializationContext}, when the + * type is derived from the

type
member not when Jackson can just infer the deserializer from the type of + * this property. */ - private F submittedForm; + @Getter + private Form submittedForm; protected ManagedForm(@JacksonInject(useInput = OptBoolean.FALSE) MetaStorage storage) { super(storage); } - public ManagedForm(F submittedForm, User owner, Dataset submittedDataset, MetaStorage storage) { + protected ManagedForm(F submittedForm, User owner, Dataset submittedDataset, MetaStorage storage) { super(owner, submittedDataset, storage); this.submittedForm = submittedForm; } - @Override - protected void doInitExecutable() { - - } @Override public void start() { @@ -79,8 +81,8 @@ public void visit(Consumer visitor) { @Override @JsonIgnore - public QueryDescription getSubmitted() { - return submittedForm; + public F getSubmitted() { + return (F) submittedForm; } diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedInternalForm.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedInternalForm.java index 807802d509..67c15c0864 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedInternalForm.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/ManagedInternalForm.java @@ -5,7 +5,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import com.bakdata.conquery.apiv1.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; import com.bakdata.conquery.apiv1.forms.Form; import com.bakdata.conquery.apiv1.forms.InternalForm; import com.bakdata.conquery.io.cps.CPSType; @@ -41,6 +41,7 @@ @Slf4j @CPSType(base = ManagedExecution.class, id = "INTERNAL_FORM") @Getter +@EqualsAndHashCode(callSuper = true) public class ManagedInternalForm extends ManagedForm implements SingleTableResult, InternalExecution { @@ -49,10 +50,11 @@ public class ManagedInternalForm extends ManagedF * This is required by forms that have multiple results (CSVs) as output. */ @JsonIgnore + @EqualsAndHashCode.Exclude private Map subQueries; /** - * Subqueries that are send to the workers. + * Subqueries that are sent to the workers. */ @JsonIgnore @EqualsAndHashCode.Exclude @@ -69,7 +71,7 @@ public ManagedInternalForm(F form, User user, Dataset submittedDataset, MetaStor @Override public void doInitExecutable() { // Convert sub queries to sub executions - getSubmittedForm().resolve(new QueryResolveContext(getNamespace(), getConfig(), getStorage(), null)); + getSubmitted().resolve(new QueryResolveContext(getNamespace(), getConfig(), getStorage(), null)); subQueries = createSubExecutions(); // Initialize sub executions @@ -78,9 +80,9 @@ public void doInitExecutable() { @NotNull private Map createSubExecutions() { - return getSubmittedForm().createSubQueries() - .entrySet() - .stream().collect(Collectors.toMap( + return getSubmitted().createSubQueries() + .entrySet() + .stream().collect(Collectors.toMap( e -> e.getKey(), e -> e.getValue().toManagedExecution(getOwner(), getDataset(), getStorage()) @@ -103,9 +105,7 @@ public List generateColumnDescriptions() { } - @Override protected void setAdditionalFieldsForStatusWithColumnDescription(Subject subject, FullExecutionStatus status) { - super.setAdditionalFieldsForStatusWithColumnDescription(subject, status); // Set the ColumnDescription if the Form only consits of a single subquery if (subQueries == null) { // If subqueries was not set the Execution was not initialized, do it manually diff --git a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/RelativeFormQueryPlan.java b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/RelativeFormQueryPlan.java index ea819fb08c..cb074823f2 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/forms/managed/RelativeFormQueryPlan.java +++ b/backend/src/main/java/com/bakdata/conquery/models/forms/managed/RelativeFormQueryPlan.java @@ -26,11 +26,13 @@ import com.google.common.collect.ImmutableList; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.ToString; import lombok.extern.slf4j.Slf4j; @Slf4j @Getter @RequiredArgsConstructor +@ToString public class RelativeFormQueryPlan implements QueryPlan { // Position of fixed columns in the result. (This is without identifier column[s], they are added upon result rendering) @@ -80,7 +82,7 @@ public Optional execute(QueryExecutionContext ctx, Entity int size = calculateCompleteLength(); EntityResult contained = preResult.get(); // Gather all validity dates from prerequisite - CDateSet dateSet = query.getValidityDateAggregator().map(Aggregator::createAggregationResult).orElseGet(CDateSet::create); + CDateSet dateSet = query.getValidityDateAggregator().map(Aggregator::createAggregationResult).orElseGet(CDateSet::createEmpty); final OptionalInt sampled = indexSelector.sample(dateSet); diff --git a/backend/src/main/java/com/bakdata/conquery/models/identifiable/ids/specific/ManagedExecutionId.java b/backend/src/main/java/com/bakdata/conquery/models/identifiable/ids/specific/ManagedExecutionId.java index 32671646eb..f0a2d62e40 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/identifiable/ids/specific/ManagedExecutionId.java +++ b/backend/src/main/java/com/bakdata/conquery/models/identifiable/ids/specific/ManagedExecutionId.java @@ -13,7 +13,7 @@ @AllArgsConstructor @Getter -@EqualsAndHashCode(callSuper = false) +@EqualsAndHashCode(callSuper = false, doNotUseGetters = true) public class ManagedExecutionId extends Id { private final DatasetId dataset; diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/AbstractIndexKey.java b/backend/src/main/java/com/bakdata/conquery/models/index/AbstractIndexKey.java index 3eb95e950f..dff8a9996f 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/AbstractIndexKey.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/AbstractIndexKey.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.index; +import java.net.URI; import java.net.URL; import lombok.Data; @@ -11,6 +12,6 @@ */ @Data public abstract class AbstractIndexKey>> implements IndexKey { - private final URL csv; + private final URI csv; private final String internalColumn; } diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/FrontendValueIndexKey.java b/backend/src/main/java/com/bakdata/conquery/models/index/FrontendValueIndexKey.java index a86928219b..5ca864c5ba 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/FrontendValueIndexKey.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/FrontendValueIndexKey.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.index; +import java.net.URI; import java.net.URL; import java.util.List; @@ -28,7 +29,7 @@ public class FrontendValueIndexKey extends AbstractIndexKey */ private final String optionValueTemplate; - public FrontendValueIndexKey(URL csv, String internalColumn, String valueTemplate, String optionValueTemplate, int suffixCutoff, String splitPattern) { + public FrontendValueIndexKey(URI csv, String internalColumn, String valueTemplate, String optionValueTemplate, int suffixCutoff, String splitPattern) { super(csv, internalColumn); this.suffixCutoff = suffixCutoff; this.splitPattern = splitPattern; diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/IndexKey.java b/backend/src/main/java/com/bakdata/conquery/models/index/IndexKey.java index 3875c58643..fda0bedaf1 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/IndexKey.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/IndexKey.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.index; +import java.net.URI; import java.net.URL; import java.util.List; @@ -11,7 +12,14 @@ * @param The type of Index that is indexed by this key */ public interface IndexKey>> { - URL getCsv(); + + /** + * An url, or a part of it, that points to the referenced csv file. + * + * @implNote This is an url but implemented as an uri in this data object, because url can have undesired + * side effects: URL equals() and hashcode() + */ + URI getCsv(); String getInternalColumn(); diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/IndexService.java b/backend/src/main/java/com/bakdata/conquery/models/index/IndexService.java index a22e7b7c50..a8c3e9bd54 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/IndexService.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/IndexService.java @@ -45,7 +45,7 @@ public Index load(@NotNull IndexKey key) throws Exception { final CsvParser csvParser = new CsvParser(csvParserSettings); - try (InputStream inputStream = key.getCsv().openStream()) { + try (InputStream inputStream = key.getCsv().toURL().openStream()) { final IterableResult records = csvParser.iterateRecords(inputStream); diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/MapIndexKey.java b/backend/src/main/java/com/bakdata/conquery/models/index/MapIndexKey.java index 836016711a..cdaaad9619 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/MapIndexKey.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/MapIndexKey.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.index; +import java.net.URI; import java.net.URL; import java.util.List; @@ -12,7 +13,7 @@ public class MapIndexKey extends AbstractIndexKey { private final String externalTemplate; - public MapIndexKey(URL csv, String internalColumn, String externalTemplate) { + public MapIndexKey(URI csv, String internalColumn, String externalTemplate) { super(csv, internalColumn); this.externalTemplate = externalTemplate; } diff --git a/backend/src/main/java/com/bakdata/conquery/models/index/MapInternToExternMapper.java b/backend/src/main/java/com/bakdata/conquery/models/index/MapInternToExternMapper.java index 238c1708a8..0ee8181697 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/index/MapInternToExternMapper.java +++ b/backend/src/main/java/com/bakdata/conquery/models/index/MapInternToExternMapper.java @@ -1,17 +1,19 @@ package com.bakdata.conquery.models.index; -import java.net.URL; +import java.net.URI; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.io.jackson.serializer.NsIdRef; +import com.bakdata.conquery.models.config.ConqueryConfig; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.identifiable.NamedImpl; import com.bakdata.conquery.models.identifiable.ids.NamespacedIdentifiable; import com.bakdata.conquery.models.identifiable.ids.specific.InternToExternMapperId; +import com.bakdata.conquery.util.io.FileUtil; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.OptBoolean; @@ -37,6 +39,10 @@ public class MapInternToExternMapper extends NamedImpl i @JacksonInject(useInput = OptBoolean.FALSE) private IndexService mapIndex; + @JsonIgnore + @JacksonInject(useInput = OptBoolean.FALSE) + private ConqueryConfig config; + @NsIdRef @Setter @NotNull @@ -47,7 +53,7 @@ public class MapInternToExternMapper extends NamedImpl i private final String name; @ToString.Include @NotNull - private final URL csv; + private final URI csv; @ToString.Include @NotEmpty private final String internalColumn; @@ -64,9 +70,14 @@ public class MapInternToExternMapper extends NamedImpl i @Override public synchronized void init() { - int2ext = mapIndex.getIndex(new MapIndexKey(csv, internalColumn, externalTemplate)); + + final URI resolvedURI = FileUtil.getResolvedUri(config.getIndex().getBaseUrl(), csv); + log.trace("Resolved mapping reference csv url '{}': {}", this.getId(), resolvedURI); + + int2ext = mapIndex.getIndex(new MapIndexKey(resolvedURI, internalColumn, externalTemplate)); } + @Override public boolean initialized() { return int2ext != null; diff --git a/backend/src/main/java/com/bakdata/conquery/models/jobs/UpdateFilterSearchJob.java b/backend/src/main/java/com/bakdata/conquery/models/jobs/UpdateFilterSearchJob.java index 12fc0ccb06..2b315e92bc 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/jobs/UpdateFilterSearchJob.java +++ b/backend/src/main/java/com/bakdata/conquery/models/jobs/UpdateFilterSearchJob.java @@ -12,10 +12,11 @@ import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import com.bakdata.conquery.apiv1.frontend.FrontendValue; import com.bakdata.conquery.io.storage.NamespaceStorage; -import com.bakdata.conquery.models.config.SearchConfig; +import com.bakdata.conquery.models.config.IndexConfig; import com.bakdata.conquery.models.datasets.concepts.Searchable; import com.bakdata.conquery.models.datasets.concepts.filters.specific.SelectFilter; import com.bakdata.conquery.util.search.TrieSearch; @@ -34,13 +35,13 @@ public class UpdateFilterSearchJob extends Job { private final NamespaceStorage storage; @NonNull - private final Map> searchCache; + private final Map, TrieSearch> searchCache; @NonNull - private final SearchConfig searchConfig; + private final IndexConfig indexConfig; @NonNull - private final Object2LongMap> totals; + private final Object2LongMap> totals; @Override public void execute() throws Exception { @@ -58,7 +59,7 @@ public void execute() throws Exception { .collect(Collectors.toList()); - final Set collectedSearchables = + final Set> collectedSearchables = allSelectFilters.stream() .map(SelectFilter::getSearchReferences) .flatMap(Collection::stream) @@ -71,12 +72,11 @@ public void execute() throws Exception { // Most computations are cheap but data intensive: we fork here to use as many cores as possible. final ExecutorService service = Executors.newCachedThreadPool(); - final Map> synchronizedResult = Collections.synchronizedMap(searchCache); + final Map, TrieSearch> synchronizedResult = Collections.synchronizedMap(searchCache); log.debug("Found {} searchable Objects.", collectedSearchables.size()); - - for (Searchable searchable : collectedSearchables) { + for (Searchable searchable : collectedSearchables) { service.submit(() -> { @@ -85,7 +85,7 @@ public void execute() throws Exception { log.info("BEGIN collecting entries for `{}`", searchable); try { - final List> values = searchable.getSearches(searchConfig, storage); + final List> values = searchable.getSearches(indexConfig, storage); for (TrieSearch search : values) { synchronizedResult.put(searchable, search); @@ -121,17 +121,23 @@ public void execute() throws Exception { // Precompute totals as that can be slow when doing it on-demand. totals.putAll( - allSelectFilters.parallelStream() - .collect(Collectors.toMap( - Functions.identity(), - filter -> filter.getSearchReferences().stream() - .map(searchCache::get) - .filter(Objects::nonNull) // Failed or disabled searches are null - .flatMap(TrieSearch::stream) - .mapToInt(FrontendValue::hashCode) - .distinct() - .count() - )) + Stream.concat( + // SelectFilters without their own labels are not "real" Searchables and therefore not in collectedSearchables + // We however want the real totals of ALL Searchables (and especially SelectFilters), which is why we include them here explicitly + allSelectFilters.parallelStream(), + collectedSearchables.parallelStream() + ) + .distinct() + .collect(Collectors.toMap( + Functions.identity(), + filter -> filter.getSearchReferences().stream() + .map(searchCache::get) + .filter(Objects::nonNull) // Failed or disabled searches are null + .flatMap(TrieSearch::stream) + .mapToInt(FrontendValue::hashCode) + .distinct() + .count() + )) ); diff --git a/backend/src/main/java/com/bakdata/conquery/models/preproc/package-info.java b/backend/src/main/java/com/bakdata/conquery/models/preproc/package-info.java index f911a37573..b88132ae4f 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/preproc/package-info.java +++ b/backend/src/main/java/com/bakdata/conquery/models/preproc/package-info.java @@ -1,18 +1,18 @@ package com.bakdata.conquery.models.preproc; /** * Note on the CQPP file format: - * + *

* It is encoded as Smile/BinaryJson-format consisting of three documents: * - {@link com.bakdata.conquery.models.preproc.PreprocessedHeader}: metadata of the import. * - {@link com.bakdata.conquery.models.preproc.PreprocessedDictionaries}: dictionary encoded strings for the import. * - {@link com.bakdata.conquery.models.preproc.PreprocessedData}: the description and raw representation of the data as {@link com.bakdata.conquery.models.events.stores.root.ColumnStore}. - * + *

* The file is split into three sections, so we can load them progressively: * Initially, we just read the header and determine if it isn't already loaded, and also fits to the {@link com.bakdata.conquery.models.datasets.Table} it is supposed to go in. * We then submit an {@link com.bakdata.conquery.models.jobs.ImportJob} which will load the data. * First the {@link com.bakdata.conquery.models.dictionary.Dictionary}s. Those are imported and are potentially altered or ingested into shared-Dictionaries (via {@link com.bakdata.conquery.models.datasets.Column#getSharedDictionary()}). - * + *

* We then load the raw data, having claims for Dictionaries in the import resolved via {@link com.bakdata.conquery.io.jackson.serializer.NsIdRef}, which is why they need to be loaded in a second step. - * + *

* TODO write the rest of the documentation for {@link com.bakdata.conquery.models.jobs.ImportJob} */ \ No newline at end of file diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/ColumnDescriptor.java b/backend/src/main/java/com/bakdata/conquery/models/query/ColumnDescriptor.java index 0074db2924..418153771b 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/ColumnDescriptor.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/ColumnDescriptor.java @@ -7,18 +7,14 @@ import com.bakdata.conquery.models.types.SemanticType; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; +import lombok.Data; /** * Container class for the query API provide meta data for reach column in the * csv result. This can be used for the result preview in the frontend. */ -@Getter +@Data @Builder -@ToString -@NoArgsConstructor @AllArgsConstructor public class ColumnDescriptor { diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/FilterSearch.java b/backend/src/main/java/com/bakdata/conquery/models/query/FilterSearch.java index 1f4f9de287..63850ff7e2 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/FilterSearch.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/FilterSearch.java @@ -10,7 +10,7 @@ import com.bakdata.conquery.apiv1.frontend.FrontendValue; import com.bakdata.conquery.io.storage.NamespaceStorage; import com.bakdata.conquery.models.config.CSVConfig; -import com.bakdata.conquery.models.config.SearchConfig; +import com.bakdata.conquery.models.config.IndexConfig; import com.bakdata.conquery.models.datasets.concepts.Searchable; import com.bakdata.conquery.models.datasets.concepts.filters.specific.SelectFilter; import com.bakdata.conquery.models.jobs.JobManager; @@ -32,7 +32,7 @@ public class FilterSearch { private final NamespaceStorage storage; private final JobManager jobManager; private final CSVConfig parserConfig; - private final SearchConfig searchConfig; + private final IndexConfig indexConfig; /** * We tag our searches based on references collected in getSearchReferences. We do not mash them all together to allow for sharing and prioritising different sources. @@ -40,14 +40,14 @@ public class FilterSearch { * In the code below, the keys of this map will usually be called "reference". */ @JsonIgnore - private final Map> searchCache = new HashMap<>(); - private Object2LongMap> totals = Object2LongMaps.emptyMap(); + private final Map, TrieSearch> searchCache = new HashMap<>(); + private Object2LongMap> totals = Object2LongMaps.emptyMap(); /** * From a given {@link FrontendValue} extract all relevant keywords. */ public static List extractKeywords(FrontendValue value) { - List keywords = new ArrayList<>(3); + final List keywords = new ArrayList<>(3); keywords.add(value.getLabel()); keywords.add(value.getValue()); @@ -62,15 +62,15 @@ public static List extractKeywords(FrontendValue value) { /** * For a {@link SelectFilter} collect all relevant {@link TrieSearch}. */ - public List> getSearchesFor(SelectFilter filter) { - return filter.getSearchReferences().stream() - .map(searchCache::get) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + public List> getSearchesFor(Searchable searchable) { + return searchable.getSearchReferences().stream() + .map(searchCache::get) + .filter(Objects::nonNull) + .collect(Collectors.toList()); } - public long getTotal(SelectFilter filter) { - return totals.getOrDefault(filter, 0); + public long getTotal(Searchable searchable) { + return totals.getOrDefault(searchable, 0); } @@ -81,7 +81,7 @@ public void updateSearch() { totals = new Object2LongOpenHashMap<>(); - jobManager.addSlowJob(new UpdateFilterSearchJob(storage, searchCache, searchConfig, totals)); + jobManager.addSlowJob(new UpdateFilterSearchJob(storage, searchCache, indexConfig, totals)); } } diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/ManagedQuery.java b/backend/src/main/java/com/bakdata/conquery/models/query/ManagedQuery.java index 7bb935af18..4bd1f5449a 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/ManagedQuery.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/ManagedQuery.java @@ -13,8 +13,8 @@ import java.util.stream.Stream; import c10n.C10N; -import com.bakdata.conquery.apiv1.ExecutionStatus; -import com.bakdata.conquery.apiv1.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.ExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; import com.bakdata.conquery.apiv1.query.Query; import com.bakdata.conquery.apiv1.query.QueryDescription; import com.bakdata.conquery.apiv1.query.SecondaryIdQuery; @@ -146,9 +146,7 @@ public void setStatusBase(@NonNull Subject subject, @NonNull ExecutionStatus sta } } - @Override protected void setAdditionalFieldsForStatusWithColumnDescription(Subject subject, FullExecutionStatus status) { - super.setAdditionalFieldsForStatusWithColumnDescription(subject, status); if (columnDescriptions == null) { columnDescriptions = generateColumnDescriptions(); } diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/QueryPlanContext.java b/backend/src/main/java/com/bakdata/conquery/models/query/QueryPlanContext.java index 4c771e2de7..2d61ca4d9c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/QueryPlanContext.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/QueryPlanContext.java @@ -23,7 +23,6 @@ public class QueryPlanContext { private CDateRange dateRestriction = CDateRange.all(); - /** * Set if in {@link com.bakdata.conquery.models.query.queryplan.SecondaryIdQueryPlan}, to the query-active {@link SecondaryIdDescriptionId}. */ diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewExecution.java b/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewExecution.java index 170515555b..5e3de93ae2 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewExecution.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewExecution.java @@ -1,23 +1,31 @@ package com.bakdata.conquery.models.query.preview; +import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; -import com.bakdata.conquery.apiv1.FullExecutionStatus; +import com.bakdata.conquery.apiv1.execution.FullExecutionStatus; import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.io.storage.MetaStorage; import com.bakdata.conquery.models.auth.entities.Subject; import com.bakdata.conquery.models.auth.entities.User; -import com.bakdata.conquery.models.config.ConqueryConfig; +import com.bakdata.conquery.models.common.CDate; +import com.bakdata.conquery.models.common.QuarterUtils; import com.bakdata.conquery.models.datasets.Dataset; import com.bakdata.conquery.models.datasets.PreviewConfig; import com.bakdata.conquery.models.execution.ManagedExecution; import com.bakdata.conquery.models.forms.managed.AbsoluteFormQuery; import com.bakdata.conquery.models.forms.managed.ManagedInternalForm; +import com.bakdata.conquery.models.forms.util.Resolution; import com.bakdata.conquery.models.i18n.I18n; +import com.bakdata.conquery.models.identifiable.ids.specific.SelectId; import com.bakdata.conquery.models.messages.namespaces.WorkerMessage; import com.bakdata.conquery.models.messages.namespaces.specific.ExecuteForm; import com.bakdata.conquery.models.query.ColumnDescriptor; @@ -25,43 +33,82 @@ import com.bakdata.conquery.models.query.PrintSettings; import com.bakdata.conquery.models.query.SingleTableResult; import com.bakdata.conquery.models.query.resultinfo.ResultInfo; +import com.bakdata.conquery.models.query.resultinfo.SelectResultInfo; import com.bakdata.conquery.models.query.results.EntityResult; import com.bakdata.conquery.models.query.results.MultilineEntityResult; import com.bakdata.conquery.models.types.SemanticType; -import com.bakdata.conquery.models.worker.Namespace; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.OptBoolean; import com.google.common.collect.MoreCollectors; +import lombok.ToString; +import org.apache.logging.log4j.util.Strings; +import org.jetbrains.annotations.NotNull; /** * Dedicated {@link ManagedExecution} to properly display/combine the two Queries submitted by {@link EntityPreviewForm}. * This mostly delegates to {@link EntityPreviewForm#VALUES_QUERY_NAME}, but embeds the result of {@link EntityPreviewForm#INFOS_QUERY_NAME} into {@link EntityPreviewStatus#getInfos()}. */ @CPSType(id = "ENTITY_PREVIEW_EXECUTION", base = ManagedExecution.class) -public class EntityPreviewExecution extends ManagedInternalForm implements SingleTableResult { +@ToString +public class EntityPreviewExecution extends ManagedInternalForm { + @ToString.Exclude private PreviewConfig previewConfig; + protected EntityPreviewExecution(@JacksonInject(useInput = OptBoolean.FALSE) MetaStorage storage) { + super(storage); + } + + public EntityPreviewExecution(EntityPreviewForm entityPreviewQuery, User user, Dataset submittedDataset, MetaStorage storage) { + super(entityPreviewQuery, user, submittedDataset, storage); + } + @Override public boolean isSystem() { // This Form should NEVER be started manually. Nor persisted return true; } - protected EntityPreviewExecution(@JacksonInject(useInput = OptBoolean.FALSE) MetaStorage storage) { - super(storage); + @Override + public void doInitExecutable() { + super.doInitExecutable(); + previewConfig = getNamespace().getPreviewConfig(); } - public EntityPreviewExecution(EntityPreviewForm entityPreviewQuery, User user, Dataset submittedDataset, MetaStorage storage) { - super(entityPreviewQuery, user, submittedDataset, storage); + /** + * Collects status of {@link EntityPreviewForm#getValuesQuery()} and {@link EntityPreviewForm#getInfoCardQuery()}. + *

+ * Most importantly to {@link EntityPreviewStatus#setInfos(List)} to for infos of entity. + */ + @Override + public FullExecutionStatus buildStatusFull(Subject subject) { + + initExecutable(getNamespace(), getConfig()); + + final EntityPreviewStatus status = new EntityPreviewStatus(); + setStatusFull(status, subject); + status.setQuery(getValuesQuery().getQuery()); + + final PrintSettings printSettings = new PrintSettings(true, I18n.LOCALE.get(), getNamespace(), getConfig(), null, previewConfig::resolveSelectLabel); + + status.setInfos(transformQueryResultToInfos(getInfoCardExecution(), printSettings)); + + status.setTimeStratifiedInfos(toChronoInfos(previewConfig, getSubQueries(), printSettings)); + + return status; + } + + @JsonIgnore + private ManagedQuery getValuesQuery() { + return getSubQueries().get(EntityPreviewForm.VALUES_QUERY_NAME); } /** * Takes a ManagedQuery, and transforms its result into a List of {@link EntityPreviewStatus.Info}. * The format of the query is an {@link AbsoluteFormQuery} containing a single line for one person. This should correspond to {@link EntityPreviewForm#VALUES_QUERY_NAME}. */ - private List transformQueryResultToInfos(ManagedQuery infoCardExecution, Namespace namespace, ConqueryConfig config) { + private List transformQueryResultToInfos(ManagedQuery infoCardExecution, PrintSettings printSettings) { // Submitted Query is a single line of an AbsoluteFormQuery => MultilineEntityResult with a single line. @@ -69,7 +116,6 @@ private List transformQueryResultToInfos(ManagedQuery final Object[] values = result.getValues().get(0); final List extraInfos = new ArrayList<>(values.length); - final PrintSettings printSettings = new PrintSettings(true, I18n.LOCALE.get(), namespace, config, null, previewConfig::resolveSelectLabel); // We are only interested in the Select results. for (int index = AbsoluteFormQuery.FEATURES_OFFSET; index < infoCardExecution.getResultInfos().size(); index++) { @@ -89,51 +135,155 @@ private List transformQueryResultToInfos(ManagedQuery return extraInfos; } - @Override - public void doInitExecutable() { - super.doInitExecutable(); - previewConfig = getNamespace().getPreviewConfig(); + @JsonIgnore + private ManagedQuery getInfoCardExecution() { + return getSubQueries().get(EntityPreviewForm.INFOS_QUERY_NAME); } - /** - * Collects status of {@link EntityPreviewForm#getValuesQuery()} and {@link EntityPreviewForm#getInfoCardQuery()}. - *

- * Most importantly to {@link EntityPreviewStatus#setInfos(List)} to for infos of entity. - */ - @Override - public FullExecutionStatus buildStatusFull(Subject subject) { + @NotNull + private List toChronoInfos(PreviewConfig previewConfig, Map subQueries, PrintSettings printSettings) { + final List timeStratifiedInfos = new ArrayList<>(); - initExecutable(getNamespace(), getConfig()); + for (PreviewConfig.TimeStratifiedSelects description : previewConfig.getTimeStratifiedSelects()) { + final ManagedQuery query = subQueries.get(description.label()); - final EntityPreviewStatus status = new EntityPreviewStatus(); - setStatusFull(status, subject); - status.setQuery(getValuesQuery().getQuery()); + final EntityResult entityResult = query.streamResults().collect(MoreCollectors.onlyElement()); - status.setInfos(transformQueryResultToInfos(getInfoCardExecution(), getNamespace(), getConfig())); + final Map select2desc = + description.selects().stream() + .collect(Collectors.toMap(PreviewConfig.InfoCardSelect::select, Function.identity())); - return status; + // Group lines by year and quarter. + final List yearEntries = createYearEntries(entityResult, query.getResultInfos(), printSettings, select2desc); + + // get descriptions, but drop everything that isn't a select result as the rest is already structured + final List columnDescriptors = createChronoColumnDescriptors(query, select2desc); + + final EntityPreviewStatus.TimeStratifiedInfos infos = + new EntityPreviewStatus.TimeStratifiedInfos(description.label(), description.description(), columnDescriptors, yearEntries); + + timeStratifiedInfos.add(infos); + } + + return timeStratifiedInfos; } - @Override - protected void setAdditionalFieldsForStatusWithColumnDescription(Subject subject, FullExecutionStatus status) { - status.setColumnDescriptions(generateColumnDescriptions()); + @NotNull + private List createYearEntries(EntityResult entityResult, List resultInfos, PrintSettings printSettings, Map select2desc) { + final Map yearLines = new HashMap<>(); + final Map> quarterLines = new HashMap<>(); + + groupLinesForResolutions(entityResult, yearLines, quarterLines); + + final Function> lineTransformer = createLineTransformer(resultInfos, select2desc, printSettings); + + final List yearEntries = new ArrayList<>(); + + yearLines.forEach((year, yearLine) -> { + + final List quarterEntries = new ArrayList<>(); + quarterLines.getOrDefault(year, Collections.emptyMap()) + .forEach((quarter, line) -> quarterEntries.add(new EntityPreviewStatus.QuarterEntry(quarter, lineTransformer.apply(line)))); + + quarterEntries.sort(Comparator.comparingInt(EntityPreviewStatus.QuarterEntry::quarter)); + + yearEntries.add(new EntityPreviewStatus.YearEntry(year, lineTransformer.apply(yearLine), quarterEntries)); + }); + + yearEntries.sort(Comparator.comparingInt(EntityPreviewStatus.YearEntry::year)); + + return yearEntries; } - @JsonIgnore - private ManagedQuery getInfoCardExecution() { - return getSubQueries().get(EntityPreviewForm.INFOS_QUERY_NAME); + /** + * Query contains both YEARS and QUARTERS lines: Group them. + */ + private static void groupLinesForResolutions(EntityResult entityResult, Map yearLines, Map> quarterLines) { + for (Object[] line : entityResult.listResultLines()) { + + // Since we know the dates are always aligned we need to only respect their starts. + final LocalDate date = CDate.toLocalDate(((List) line[AbsoluteFormQuery.TIME_INDEX]).get(0)); + + final int year = date.getYear(); + final int quarter = QuarterUtils.getQuarter(date); + + switch (Resolution.valueOf((String) line[AbsoluteFormQuery.RESOLUTION_INDEX])) { + case YEARS -> yearLines.put(year, line); + case QUARTERS -> quarterLines.computeIfAbsent(year, (ignored) -> new HashMap<>(4)).put(quarter, line); + default -> throw new IllegalStateException("Query may only have modes for Quarter and/or Year."); + } + } } - @JsonIgnore - private ManagedQuery getValuesQuery() { - return getSubQueries().get(EntityPreviewForm.VALUES_QUERY_NAME); + /** + * Creates a transformer printing lines, transformed into a Map of label->value. + * Null values are omitted. + */ + private static Function> createLineTransformer(List resultInfos, Map select2desc, PrintSettings printSettings) { + + + final int size = resultInfos.size(); + final String[] columnNames = new String[size]; + + for (int index = 0; index < size; index++) { + final ResultInfo resultInfo = resultInfos.get(index); + + if (resultInfo instanceof SelectResultInfo selectResultInfo) { + columnNames[index] = select2desc.get(selectResultInfo.getSelect().getId()).label(); + } + } + + return line -> { + final Map out = new HashMap<>(size); + + for (int column = 0; column < size; column++) { + final String columnName = columnNames[column]; + + if (columnName == null) { + continue; + } + + final String value = resultInfos.get(column).getType().printNullable(printSettings, line[column]); + + if (Strings.isBlank(value)) { + continue; + } + + out.put(columnName, value); + } + + return out; + }; } + /** + * For the selects in result infos, build ColumnDescriptors using definitions (label and description) from PreviewConfig. + */ + private static List createChronoColumnDescriptors(SingleTableResult query, Map select2desc) { + + final List columnDescriptions = new ArrayList<>(); + + for (ResultInfo info : query.getResultInfos()) { + if (info instanceof SelectResultInfo selectResultInfo) { + final PreviewConfig.InfoCardSelect desc = select2desc.get(selectResultInfo.getSelect().getId()); + + // We build these by hand because they are labeled and described by config. + columnDescriptions.add(ColumnDescriptor.builder() + .label(desc.label()) + .defaultLabel(desc.label()) + .type(info.getType().typeInfo()) + .semantics(info.getSemantics()) + .description(desc.description()) + .build()); + } + } - @Override - public WorkerMessage createExecutionMessage() { - return new ExecuteForm(getId(), getFlatSubQueries().entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getQuery()))); + + return columnDescriptions; + } + + protected void setAdditionalFieldsForStatusWithColumnDescription(Subject subject, FullExecutionStatus status) { + status.setColumnDescriptions(generateColumnDescriptions()); } @Override @@ -141,6 +291,7 @@ public List generateColumnDescriptions() { final List descriptors = getValuesQuery().generateColumnDescriptions(); for (ColumnDescriptor descriptor : descriptors) { + // Add grouping semantics to secondaryIds to group by if (descriptor.getSemantics() .stream() .anyMatch(semanticType -> semanticType instanceof SemanticType.SecondaryIdT desc @@ -148,6 +299,7 @@ public List generateColumnDescriptions() { descriptor.getSemantics().add(new SemanticType.GroupT()); } + // Add hidden semantics to fields flagged for hiding. if (descriptor.getSemantics() .stream() .anyMatch(semanticType -> semanticType instanceof SemanticType.ColumnT desc && previewConfig.isHidden(desc.getColumn()))) { @@ -159,6 +311,18 @@ public List generateColumnDescriptions() { return descriptors; } + @Override + protected void setAdditionalFieldsForStatusWithSource(Subject subject, FullExecutionStatus status) { + status.setColumnDescriptions(generateColumnDescriptions()); + } + + @Override + public WorkerMessage createExecutionMessage() { + return new ExecuteForm(getId(), getFlatSubQueries().entrySet() + .stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().getQuery()))); + } + @Override public List getResultInfos() { return getValuesQuery().getResultInfos(); diff --git a/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewForm.java b/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewForm.java index 0000acc018..bcc457b88c 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewForm.java +++ b/backend/src/main/java/com/bakdata/conquery/models/query/preview/EntityPreviewForm.java @@ -1,6 +1,8 @@ package com.bakdata.conquery.models.query.preview; import java.time.LocalDate; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -23,6 +25,7 @@ import com.bakdata.conquery.models.auth.entities.User; import com.bakdata.conquery.models.common.Range; import com.bakdata.conquery.models.datasets.Dataset; +import com.bakdata.conquery.models.datasets.PreviewConfig; import com.bakdata.conquery.models.datasets.concepts.Connector; import com.bakdata.conquery.models.datasets.concepts.select.Select; import com.bakdata.conquery.models.execution.ManagedExecution; @@ -33,13 +36,14 @@ import com.bakdata.conquery.models.query.QueryResolveContext; import com.bakdata.conquery.models.query.Visitable; import com.bakdata.conquery.models.query.visitor.QueryVisitor; +import com.bakdata.conquery.models.worker.DatasetRegistry; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.ClassToInstanceMap; -import com.google.common.collect.Sets; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** @@ -61,49 +65,90 @@ public class EntityPreviewForm extends Form implements InternalForm { public static final String INFOS_QUERY_NAME = "INFOS"; public static final String VALUES_QUERY_NAME = "VALUES"; + private final AbsoluteFormQuery infoCardQuery; private final TableExportQuery valuesQuery; + private final Map timeOverViews; + @Nullable @Override public JsonNode getValues() { return null; // will not be implemented. } - public static EntityPreviewForm create(String entity, String idKind, Range dateRange, List sources, List infos, List timeStratifiedSelects, DatasetRegistry datasetRegistry) { // We use this query to filter for the single selected query. final Query entitySelectQuery = new ConceptQuery(new CQExternal(List.of(idKind), new String[][]{{"HEAD"}, {entity}}, true)); + final TableExportQuery exportQuery = createExportQuery(dateRange, sources, entitySelectQuery); + + final AbsoluteFormQuery infoCardQuery = createInfoCardQuery(dateRange, infos, entitySelectQuery); + + final Map timeQueries = createTimeStratifiedQueries(dateRange, timeStratifiedSelects, datasetRegistry, entitySelectQuery); + + return new EntityPreviewForm(infoCardQuery, exportQuery, timeQueries); + } + + @NotNull + private static Map createTimeStratifiedQueries(Range dateRange, List timeStratifiedSelects, DatasetRegistry datasetRegistry, Query entitySelectQuery) { + final Map timeQueries = new HashMap<>(); + + // per group create an AbsoluteFormQuery on years and quarters. + for (PreviewConfig.TimeStratifiedSelects selects : timeStratifiedSelects) { + + final AbsoluteFormQuery query = new AbsoluteFormQuery(entitySelectQuery, dateRange, + ArrayConceptQuery.createFromFeatures( + selects.selects().stream() + .map(PreviewConfig.InfoCardSelect::select) + .map(datasetRegistry::resolve) + .map(CQConcept::forSelect) + .collect(Collectors.toList())), + List.of(ExportForm.ResolutionAndAlignment.of(Resolution.YEARS, Alignment.YEAR), ExportForm.ResolutionAndAlignment.of(Resolution.QUARTERS, Alignment.QUARTER)) + ); + + timeQueries.put(selects.label(), query); + } + return timeQueries; + } + + @NotNull + private static AbsoluteFormQuery createInfoCardQuery(Range dateRange, Lists in some browsers, due to the limited stylability of ``s in IE10+.\n &::-ms-expand {\n background-color: transparent;\n border: 0;\n }\n\n // Customize the `:focus` state to imitate native WebKit styles.\n @include form-control-focus();\n\n // Placeholder\n &::placeholder {\n color: $input-placeholder-color;\n // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526.\n opacity: 1;\n }\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &:disabled,\n &[readonly] {\n background-color: $input-disabled-bg;\n // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655.\n opacity: 1;\n }\n}\n\nselect.form-control {\n &:focus::-ms-value {\n // Suppress the nested default white text on blue background highlight given to\n // the selected option text when the (still closed) receives focus\n // in IE and (under certain conditions) Edge.\n // See https://github.com/twbs/bootstrap/issues/19398.\n color: $input-color;\n background-color: $input-bg;\n }\n }\n\n &[multiple],\n &[size]:not([size=\"1\"]) {\n height: auto;\n padding-right: $custom-select-padding-x;\n background-image: none;\n }\n\n &:disabled {\n color: $custom-select-disabled-color;\n background-color: $custom-select-disabled-bg;\n }\n\n // Hides the default caret in IE11\n &::-ms-expand {\n display: none;\n }\n}\n\n.custom-select-sm {\n height: $custom-select-height-sm;\n padding-top: $custom-select-padding-y-sm;\n padding-bottom: $custom-select-padding-y-sm;\n padding-left: $custom-select-padding-x-sm;\n @include font-size($custom-select-font-size-sm);\n}\n\n.custom-select-lg {\n height: $custom-select-height-lg;\n padding-top: $custom-select-padding-y-lg;\n padding-bottom: $custom-select-padding-y-lg;\n padding-left: $custom-select-padding-x-lg;\n @include font-size($custom-select-font-size-lg);\n}\n\n\n// File\n//\n// Custom file input.\n\n.custom-file {\n position: relative;\n display: inline-block;\n width: 100%;\n height: $custom-file-height;\n margin-bottom: 0;\n}\n\n.custom-file-input {\n position: relative;\n z-index: 2;\n width: 100%;\n height: $custom-file-height;\n margin: 0;\n opacity: 0;\n\n &:focus ~ .custom-file-label {\n border-color: $custom-file-focus-border-color;\n box-shadow: $custom-file-focus-box-shadow;\n }\n\n &:disabled ~ .custom-file-label {\n background-color: $custom-file-disabled-bg;\n }\n\n @each $lang, $value in $custom-file-text {\n &:lang(#{$lang}) ~ .custom-file-label::after {\n content: $value;\n }\n }\n\n ~ .custom-file-label[data-browse]::after {\n content: attr(data-browse);\n }\n}\n\n.custom-file-label {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n height: $custom-file-height;\n padding: $custom-file-padding-y $custom-file-padding-x;\n font-family: $custom-file-font-family;\n font-weight: $custom-file-font-weight;\n line-height: $custom-file-line-height;\n color: $custom-file-color;\n background-color: $custom-file-bg;\n border: $custom-file-border-width solid $custom-file-border-color;\n @include border-radius($custom-file-border-radius);\n @include box-shadow($custom-file-box-shadow);\n\n &::after {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n z-index: 3;\n display: block;\n height: $custom-file-height-inner;\n padding: $custom-file-padding-y $custom-file-padding-x;\n line-height: $custom-file-line-height;\n color: $custom-file-button-color;\n content: \"Browse\";\n @include gradient-bg($custom-file-button-bg);\n border-left: inherit;\n @include border-radius(0 $custom-file-border-radius $custom-file-border-radius 0);\n }\n}\n\n// Range\n//\n// Style range inputs the same across browsers. Vendor-specific rules for pseudo\n// elements cannot be mixed. As such, there are no shared styles for focus or\n// active states on prefixed selectors.\n\n.custom-range {\n width: 100%;\n height: calc(#{$custom-range-thumb-height} + #{$custom-range-thumb-focus-box-shadow-width * 2});\n padding: 0; // Need to reset padding\n background-color: transparent;\n appearance: none;\n\n &:focus {\n outline: none;\n\n // Pseudo-elements must be split across multiple rulesets to have an effect.\n // No box-shadow() mixin for focus accessibility.\n &::-webkit-slider-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-moz-range-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n &::-ms-thumb { box-shadow: $custom-range-thumb-focus-box-shadow; }\n }\n\n &::-moz-focus-outer {\n border: 0;\n }\n\n &::-webkit-slider-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: ($custom-range-track-height - $custom-range-thumb-height) / 2; // Webkit specific\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-webkit-slider-runnable-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent; // Why?\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent;\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-moz-range-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-moz-range-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: $custom-range-track-bg;\n border-color: transparent; // Firefox specific?\n @include border-radius($custom-range-track-border-radius);\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-thumb {\n width: $custom-range-thumb-width;\n height: $custom-range-thumb-height;\n margin-top: 0; // Edge specific\n margin-right: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n margin-left: $custom-range-thumb-focus-box-shadow-width; // Workaround that overflowed box-shadow is hidden.\n @include gradient-bg($custom-range-thumb-bg);\n border: $custom-range-thumb-border;\n @include border-radius($custom-range-thumb-border-radius);\n @include box-shadow($custom-range-thumb-box-shadow);\n @include transition($custom-forms-transition);\n appearance: none;\n\n &:active {\n @include gradient-bg($custom-range-thumb-active-bg);\n }\n }\n\n &::-ms-track {\n width: $custom-range-track-width;\n height: $custom-range-track-height;\n color: transparent;\n cursor: $custom-range-track-cursor;\n background-color: transparent;\n border-color: transparent;\n border-width: $custom-range-thumb-height / 2;\n @include box-shadow($custom-range-track-box-shadow);\n }\n\n &::-ms-fill-lower {\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &::-ms-fill-upper {\n margin-right: 15px; // arbitrary?\n background-color: $custom-range-track-bg;\n @include border-radius($custom-range-track-border-radius);\n }\n\n &:disabled {\n &::-webkit-slider-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-webkit-slider-runnable-track {\n cursor: default;\n }\n\n &::-moz-range-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n\n &::-moz-range-track {\n cursor: default;\n }\n\n &::-ms-thumb {\n background-color: $custom-range-thumb-disabled-bg;\n }\n }\n}\n\n.custom-control-label::before,\n.custom-file-label,\n.custom-select {\n @include transition($custom-forms-transition);\n}\n","// Base class\n//\n// Kickstart any navigation component with a set of style resets. Works with\n// `