diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index be45b33617..50eb598c83 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -21,6 +21,13 @@ include::content/docs/variables.adoc-include[] Starting with version 2.0.0 clustering with OrientDB is no longer part of the professional support by Gentics. For high availability setups, it is recommended to use link:https://getmesh.io/premium-features/sql-db/[Gentics Mesh SQL]. +[[v2.0.2]] +== 2.0.2 (26.07.2023) + +icon:check[] Core: Uniqueness checks for webroot url field values will now only be done, if those values actually change. This will improve performance of e.g. schema migrations, where the webroot url field values are likely to not change. + +icon:check[] Core: An internal API for efficient loading of list field values has been added. + [[v2.0.1]] == 2.0.1 (13.07.2023) diff --git a/LTS-CHANGELOG.adoc b/LTS-CHANGELOG.adoc index 54c7ebf50c..0d66ac33e6 100644 --- a/LTS-CHANGELOG.adoc +++ b/LTS-CHANGELOG.adoc @@ -17,6 +17,13 @@ include::content/docs/variables.adoc-include[] The LTS changelog lists releases which are only accessible via a commercial subscription. All fixes and changes in LTS releases will be released the next minor release. Changes from LTS 1.4.x will be included in release 1.5.0. +[[v1.10.12]] +== 1.10.12 (26.07.2023) + +icon:check[] Core: Uniqueness checks for webroot url field values will now only be done, if those values actually change. This will improve performance of e.g. schema migrations, where the webroot url field values are likely to not change. + +icon:check[] Core: An internal API for efficient loading of list field values has been added. + [[v1.10.11]] == 1.10.11 (12.07.2023) diff --git a/doc/src/main/docs/generated/tables/mesh-db-revs.adoc-include b/doc/src/main/docs/generated/tables/mesh-db-revs.adoc-include index e59d200c48..2c04133172 100644 --- a/doc/src/main/docs/generated/tables/mesh-db-revs.adoc-include +++ b/doc/src/main/docs/generated/tables/mesh-db-revs.adoc-include @@ -5,7 +5,7 @@ | Database revision -| *2.0.0* +| *2.0.1* | 6d5ccff3 | *2.0.1* diff --git a/doc/src/main/docs/generated/tables/mesh-env.adoc-include b/doc/src/main/docs/generated/tables/mesh-env.adoc-include index 2cd48ec3f1..d9304d218b 100644 --- a/doc/src/main/docs/generated/tables/mesh-env.adoc-include +++ b/doc/src/main/docs/generated/tables/mesh-env.adoc-include @@ -83,12 +83,12 @@ | *MESH_MIGRATION_TRIGGER_INTERVAL* | Override the migration trigger interval -| *MESH_CACHE_PATH_SIZE* -| Override the path cache size. - | *MESH_GRAPH_EXPORT_DIRECTORY* | Override the graph database export directory. +| *MESH_CACHE_PATH_SIZE* +| Override the path cache size. + | *MESH_VERTX_EVENT_BUS_ERROR_THRESHOLD* | Override the Vert.x eventBus error threshold in ms. @@ -149,12 +149,12 @@ | *MESH_ELASTICSEARCH_USERNAME* | Override the configured Elasticsearch connection username. -| *MESH_CLUSTER_TOPOLOGY_LOCK_TIMEOUT* -| Override the cluster topology lock timeout in ms. - | *MESH_GRAPH_BACKUP_DIRECTORY* | Override the graph database backup directory. +| *MESH_CLUSTER_TOPOLOGY_LOCK_TIMEOUT* +| Override the cluster topology lock timeout in ms. + | *MESH_VERTX_EVENT_BUS_CHECK_INTERVAL* | Override the Vert.x eventBus check interval in ms. @@ -218,12 +218,12 @@ | *MESH_HTTP_CORS_ENABLE* | Override the configured CORS enable flag. -| *MESH_HTTP_SSL_ENABLE* -| Override the configured https server flag. - | *MESH_GRAPH_STARTSERVER* | Override the graph database server flag. +| *MESH_HTTP_SSL_ENABLE* +| Override the configured https server flag. + | *MESH_HTTP_VERTICLE_AMOUNT* | Override the http verticle amount. diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/dao/ContentDao.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/dao/ContentDao.java index dc861416ef..6d782a7ce2 100644 --- a/mdm/api/src/main/java/com/gentics/mesh/core/data/dao/ContentDao.java +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/dao/ContentDao.java @@ -13,6 +13,8 @@ import java.util.Set; import java.util.stream.Stream; +import org.apache.commons.lang.NotImplementedException; + import com.gentics.mesh.context.BulkActionContext; import com.gentics.mesh.context.InternalActionContext; import com.gentics.mesh.context.impl.DummyBulkActionContext; @@ -583,7 +585,21 @@ default String getDocumentId(HibNodeFieldContainer content) { * @param conflictI18n * key of the message in case of conflicts */ - void updateWebrootPathInfo(HibNodeFieldContainer content, InternalActionContext ac, String branchUuid, String conflictI18n); + default void updateWebrootPathInfo(HibNodeFieldContainer content, InternalActionContext ac, String branchUuid, String conflictI18n) { + updateWebrootPathInfo(content, ac, branchUuid, conflictI18n, true); + } + + /** + * Update the property webroot path info. This will optionally also check for uniqueness conflicts of the webroot path and will throw a + * {@link Errors#conflict(String, String, String, String...)} if one found. + * @param ac + * @param branchUuid + * branch Uuid + * @param conflictI18n + * key of the message in case of conflicts + * @param checkForConflicts true to check for conflicts, false to omit the check + */ + void updateWebrootPathInfo(HibNodeFieldContainer content, InternalActionContext ac, String branchUuid, String conflictI18n, boolean checkForConflicts); /** * Update the property webroot path info. This will also check for uniqueness conflicts of the webroot path and will throw a @@ -593,7 +609,18 @@ default String getDocumentId(HibNodeFieldContainer content) { * @param conflictI18n */ default void updateWebrootPathInfo(HibNodeFieldContainer content, String branchUuid, String conflictI18n) { - updateWebrootPathInfo(content, null, branchUuid, conflictI18n); + updateWebrootPathInfo(content, null, branchUuid, conflictI18n, true); + } + + /** + * Update the property webroot path info. This will optionally also check for uniqueness conflicts of the webroot path and will throw a + * {@link Errors#conflict(String, String, String, String...)} if one found. + * @param branchUuid + * @param conflictI18n + * @param checkForConflicts true to check for conflicts, false to omit the check + */ + default void updateWebrootPathInfo(HibNodeFieldContainer content, String branchUuid, String conflictI18n, boolean checkForConflicts) { + updateWebrootPathInfo(content, null, branchUuid, conflictI18n, checkForConflicts); } /** @@ -1007,4 +1034,50 @@ default void purge(HibNodeFieldContainer content) { * @return */ FieldMap getFieldMap(HibNodeFieldContainer fieldContainer, InternalActionContext ac, SchemaModel schema, int level, List containerLanguageTags); + + /** + * Whether prefetching of list field values is supported. If this returns + * false, the methods {@link #getBooleanListFieldValues(List)}, + * {@link #getDateListFieldValues(List)}, {@link #getNumberListFieldValues(List)}, + * {@link #getHtmlListFieldValues(List)} and {@link #getStringListFieldValues(List)} will + * all throw a {@link NotImplementedException} when called. + * + * @return true when prefetching list field values is supported, false if not + */ + boolean supportsPrefetchingListFieldValues(); + + /** + * Get the boolean list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of boolean values + */ + Map> getBooleanListFieldValues(List listUuids); + + /** + * Get the date list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of date field values + */ + Map> getDateListFieldValues(List listUuids); + + /** + * Get the number list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of number field values + */ + Map> getNumberListFieldValues(List listUuids); + + /** + * Get the html list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of html field values + */ + Map> getHtmlListFieldValues(List listUuids); + + /** + * Get the string list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of string field values + */ + Map> getStringListFieldValues(List listUuids); } diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingContentDao.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingContentDao.java index e59fcc6512..6e7ccf44c2 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingContentDao.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingContentDao.java @@ -527,19 +527,19 @@ private NodeMeshEventModel createEvent(MeshEvent event, HibNodeFieldContainer co } @Override - default void updateWebrootPathInfo(HibNodeFieldContainer content, InternalActionContext ac, String branchUuid, String conflictI18n) { + default void updateWebrootPathInfo(HibNodeFieldContainer content, InternalActionContext ac, String branchUuid, String conflictI18n, boolean checkForConflicts) { Set urlFieldValues = getUrlFieldValues(content).collect(Collectors.toSet()); Iterator it = getContainerEdges(content, DRAFT, branchUuid); if (it.hasNext()) { HibNodeFieldContainerEdge draftEdge = it.next(); updateWebrootPathInfo(content, ac, draftEdge, branchUuid, conflictI18n, DRAFT); - updateWebrootUrlFieldsInfo(content, draftEdge, branchUuid, urlFieldValues, DRAFT); + updateWebrootUrlFieldsInfo(content, draftEdge, branchUuid, urlFieldValues, DRAFT, checkForConflicts); } it = getContainerEdges(content, PUBLISHED, branchUuid); if (it.hasNext()) { HibNodeFieldContainerEdge publishEdge = it.next(); updateWebrootPathInfo(content, ac, publishEdge, branchUuid, conflictI18n, PUBLISHED); - updateWebrootUrlFieldsInfo(content, publishEdge, branchUuid, urlFieldValues, PUBLISHED); + updateWebrootUrlFieldsInfo(content, publishEdge, branchUuid, urlFieldValues, PUBLISHED, checkForConflicts); } } @@ -708,28 +708,31 @@ default String composeSegmentInfo(HibNode parentNode, String segment) { * @param branchUuid * @param urlFieldValues * @param type + * @param checkForConflicts true when the check for conflicting values must be done, false if not */ - private void updateWebrootUrlFieldsInfo(HibNodeFieldContainer content, HibNodeFieldContainerEdge edge, String branchUuid, Set urlFieldValues, ContainerType type) { + private void updateWebrootUrlFieldsInfo(HibNodeFieldContainer content, HibNodeFieldContainerEdge edge, String branchUuid, Set urlFieldValues, ContainerType type, boolean checkForConflicts) { if (urlFieldValues != null && !urlFieldValues.isEmpty()) { - HibNodeFieldContainerEdge conflictingEdge = getConflictingEdgeOfWebrootField(content, edge, urlFieldValues, branchUuid, type); - if (conflictingEdge != null) { - HibNodeFieldContainer conflictingContainer = conflictingEdge.getNodeContainer(); - HibNode conflictingNode = conflictingEdge.getNode(); - if (log.isDebugEnabled()) { - log.debug( - "Found conflicting container with uuid {" + conflictingContainer.getUuid() + "} of node {" + conflictingNode.getUuid()); + if (checkForConflicts) { + HibNodeFieldContainerEdge conflictingEdge = getConflictingEdgeOfWebrootField(content, edge, urlFieldValues, branchUuid, type); + if (conflictingEdge != null) { + HibNodeFieldContainer conflictingContainer = conflictingEdge.getNodeContainer(); + HibNode conflictingNode = conflictingEdge.getNode(); + if (log.isDebugEnabled()) { + log.debug( + "Found conflicting container with uuid {" + conflictingContainer.getUuid() + "} of node {" + conflictingNode.getUuid()); + } + // We know that the found container already occupies the index with one of the given paths. Lets compare both sets of paths in order to + // determine + // which path caused the conflict. + Set fromConflictingContainer = getUrlFieldValues(conflictingContainer).collect(Collectors.toSet()); + @SuppressWarnings("unchecked") + Collection conflictingValues = CollectionUtils.intersection(fromConflictingContainer, urlFieldValues); + String paths = String.join(",", conflictingValues); + + throw nodeConflict(conflictingNode.getUuid(), conflictingContainer.getDisplayFieldValue(), conflictingContainer.getLanguageTag(), + "node_conflicting_urlfield_update", paths, conflictingContainer.getNode().getUuid(), + conflictingContainer.getLanguageTag()); } - // We know that the found container already occupies the index with one of the given paths. Lets compare both sets of paths in order to - // determine - // which path caused the conflict. - Set fromConflictingContainer = getUrlFieldValues(conflictingContainer).collect(Collectors.toSet()); - @SuppressWarnings("unchecked") - Collection conflictingValues = CollectionUtils.intersection(fromConflictingContainer, urlFieldValues); - String paths = String.join(",", conflictingValues); - - throw nodeConflict(conflictingNode.getUuid(), conflictingContainer.getDisplayFieldValue(), conflictingContainer.getLanguageTag(), - "node_conflicting_urlfield_update", paths, conflictingContainer.getNode().getUuid(), - conflictingContainer.getLanguageTag()); } edge.setUrlFieldInfo(urlFieldValues); } else { diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingNodeDao.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingNodeDao.java index ba5e4a9ccf..27342173f2 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingNodeDao.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/dao/PersistingNodeDao.java @@ -42,6 +42,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; + +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -1956,13 +1958,15 @@ default void setPublished(HibNode node, InternalActionContext ac, HibNodeFieldCo PersistingContentDao contentDao = CommonTx.get().contentDao(); String languageTag = container.getLanguageTag(); boolean isAutoPurgeEnabled = contentDao.isAutoPurgeEnabled(container); + Set oldUrlFieldValues = Collections.emptySet(); // Remove an existing published edge HibNodeFieldContainerEdge edge = contentDao.getEdge(node, languageTag, branchUuid, PUBLISHED); if (edge != null) { HibNodeFieldContainer oldPublishedContainer = contentDao.getFieldContainerOfEdge(edge); + oldUrlFieldValues = contentDao.getUrlFieldValues(oldPublishedContainer).collect(Collectors.toSet()); contentDao.removeEdge(edge); - contentDao.updateWebrootPathInfo(oldPublishedContainer, branchUuid, "node_conflicting_segmentfield_publish"); + contentDao.updateWebrootPathInfo(oldPublishedContainer, branchUuid, "node_conflicting_segmentfield_publish", true); if (ac.isPurgeAllowed() && isAutoPurgeEnabled && contentDao.isPurgeable(oldPublishedContainer)) { contentDao.purge(oldPublishedContainer); } @@ -1976,7 +1980,11 @@ default void setPublished(HibNode node, InternalActionContext ac, HibNodeFieldCo } // create new published edge contentDao.createContainerEdge(node, container, branchUuid, languageTag, PUBLISHED); - contentDao.updateWebrootPathInfo(container, branchUuid, "node_conflicting_segmentfield_publish"); + + // only check for conflicts, when the values are different from the values of the old container + Set newUrlFieldValues = contentDao.getUrlFieldValues(container).collect(Collectors.toSet()); + boolean checkForConflicts = !CollectionUtils.isEqualCollection(oldUrlFieldValues, newUrlFieldValues); + contentDao.updateWebrootPathInfo(container, branchUuid, "node_conflicting_segmentfield_publish", checkForConflicts); } @Override diff --git a/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/ContentDaoWrapperImpl.java b/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/ContentDaoWrapperImpl.java index 56045935e9..8948c06a72 100644 --- a/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/ContentDaoWrapperImpl.java +++ b/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/ContentDaoWrapperImpl.java @@ -15,6 +15,7 @@ import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -360,4 +361,34 @@ public void deleteField(HibDeletableField field) { public void setDisplayFieldValue(HibNodeFieldContainer container, String value) { toGraph(container).property(NodeGraphFieldContainerImpl.DISPLAY_FIELD_PROPERTY_KEY, value); } + + @Override + public boolean supportsPrefetchingListFieldValues() { + return false; + } + + @Override + public Map> getBooleanListFieldValues(List listUuids) { + throw new NotImplementedException("Prefetching of list values is not implemented"); + } + + @Override + public Map> getDateListFieldValues(List listUuids) { + throw new NotImplementedException("Prefetching of list values is not implemented"); + } + + @Override + public Map> getNumberListFieldValues(List listUuids) { + throw new NotImplementedException("Prefetching of list values is not implemented"); + } + + @Override + public Map> getHtmlListFieldValues(List listUuids) { + throw new NotImplementedException("Prefetching of list values is not implemented"); + } + + @Override + public Map> getStringListFieldValues(List listUuids) { + throw new NotImplementedException("Prefetching of list values is not implemented"); + } } diff --git a/tests/common/src/main/java/com/gentics/mesh/assertj/impl/JsonObjectAssert.java b/tests/common/src/main/java/com/gentics/mesh/assertj/impl/JsonObjectAssert.java index d52398a95a..84a31efd63 100644 --- a/tests/common/src/main/java/com/gentics/mesh/assertj/impl/JsonObjectAssert.java +++ b/tests/common/src/main/java/com/gentics/mesh/assertj/impl/JsonObjectAssert.java @@ -1,6 +1,7 @@ package com.gentics.mesh.assertj.impl; import static com.gentics.mesh.MeshVersion.CURRENT_API_BASE_PATH; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -9,13 +10,17 @@ import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Scanner; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.AbstractAssert; import org.jetbrains.annotations.NotNull; @@ -80,8 +85,27 @@ public JsonObjectAssert hasNot(String path, String msg) { public JsonObjectAssert has(String path, String value, String msg) { try { Object actualValue = getByPath(path); - String actualStringRep = String.valueOf(actualValue); - assertEquals("Value for property on path {" + path + "} did not match: " + msg, value, actualStringRep); + if (actualValue instanceof Collection) { + Collection actualCollection = (Collection) actualValue; + + if (StringUtils.startsWith(value, "[") && StringUtils.endsWith(value, "]")) { + value = StringUtils.removeStart(value, "["); + value = StringUtils.removeEnd(value, "]"); + String[] valueParts = StringUtils.split(value, ","); + for (int i = 0; i < valueParts.length; i++) { + valueParts[i] = StringUtils.trim(valueParts[i]); + } + List values = Arrays.asList(valueParts); + assertThat(actualCollection).as("Value for property on path {" + path + "}").containsOnlyElementsOf(values); + } else { + fail("Expected value for path {" + path + "} should be an array (eclosed by '[' and ']') but was {" + + value + "}"); + } + + } else { + String actualStringRep = String.valueOf(actualValue); + assertEquals("Value for property on path {" + path + "} did not match: " + msg, value, actualStringRep); + } } catch (PathNotFoundException e) { fail("Could not find property for path {" + path + "} - Json is:\n--snip--\n" + actual.encodePrettily() + "\n--snap--\n" + msg); } diff --git a/tests/common/src/main/java/com/gentics/mesh/test/ClientHelper.java b/tests/common/src/main/java/com/gentics/mesh/test/ClientHelper.java index a656c24a9e..740532eb8a 100644 --- a/tests/common/src/main/java/com/gentics/mesh/test/ClientHelper.java +++ b/tests/common/src/main/java/com/gentics/mesh/test/ClientHelper.java @@ -11,6 +11,7 @@ import static org.junit.Assert.fail; import java.util.Locale; +import java.util.Objects; import java.util.function.Function; import com.gentics.mesh.core.data.i18n.I18NUtil; @@ -246,4 +247,18 @@ public static void expectFailureMessage(Throwable e, HttpResponseStatus status, fail("Unhandled exception"); } } + + public static boolean isFailureMessage(Throwable e, HttpResponseStatus status) { + if (e instanceof GenericRestException) { + GenericRestException exception = (GenericRestException) e; + return Objects.equals(status, exception.getStatus()); + } else if (e instanceof MeshRestClientMessageException) { + MeshRestClientMessageException exception = (MeshRestClientMessageException) e; + return Objects.equals(status.code(), exception.getStatusCode()); + } else if (e != null && e.getCause() != null && e.getCause() != e) { + return isFailureMessage(e.getCause(), status); + } else { + return false; + } + } } diff --git a/tests/context-orientdb/src/main/java/com/gentics/mesh/core/project/OrientDBProjectEndpointTest.java b/tests/context-orientdb/src/main/java/com/gentics/mesh/core/project/OrientDBProjectEndpointTest.java index 3a6dad9b77..04d0b8e2ab 100644 --- a/tests/context-orientdb/src/main/java/com/gentics/mesh/core/project/OrientDBProjectEndpointTest.java +++ b/tests/context-orientdb/src/main/java/com/gentics/mesh/core/project/OrientDBProjectEndpointTest.java @@ -1,24 +1,31 @@ package com.gentics.mesh.core.project; import static com.gentics.mesh.test.ClientHelper.call; +import static com.gentics.mesh.test.ClientHelper.isFailureMessage; import static com.gentics.mesh.test.ElasticsearchTestMode.TRACKING; import static org.assertj.core.api.Assertions.assertThat; +import java.util.concurrent.TimeoutException; + import org.junit.Test; import com.gentics.mesh.core.rest.project.ProjectResponse; import com.gentics.mesh.test.MeshTestSetting; import com.gentics.mesh.test.TestSize; +import io.netty.handler.codec.http.HttpResponseStatus; + @MeshTestSetting(elasticsearch = TRACKING, testSize = TestSize.FULL, startServer = true) public class OrientDBProjectEndpointTest extends ProjectEndpointTest { /** * Test renaming, deleting and re-creating a project (together with project name cache). + * @throws InterruptedException + * @throws TimeoutException */ @Test - public void testRenameDeleteCreateProject() { + public void testRenameDeleteCreateProject() throws InterruptedException, TimeoutException { // create project named "project" ProjectResponse project = createProject("project"); @@ -30,8 +37,25 @@ public void testRenameDeleteCreateProject() { project = updateProject(project.getUuid(), "newproject"); assertThat(mesh().projectNameCache().size()).as("Project name cache size").isEqualTo(0); + long maxWaitMs = 1000; + long delayMs = 100; + boolean repeat = false; + // get tag families of newproject (this will put project into cache) - call(() -> client().findTagFamilies("newproject")); + long start = System.currentTimeMillis(); + do { + try { + call(() -> client().findTagFamilies("newproject")); + repeat = false; + } catch (Throwable t) { + if (isFailureMessage(t, HttpResponseStatus.NOT_FOUND) && (System.currentTimeMillis() - start) < maxWaitMs) { + Thread.sleep(delayMs); + repeat = true; + } else { + throw t; + } + } + } while (repeat); assertThat(mesh().projectNameCache().size()).as("Project name cache size").isEqualTo(1); // delete "newproject" diff --git a/tests/tests-core/src/main/resources/graphql/node-tag-query b/tests/tests-core/src/main/resources/graphql/node-tag-query index 5c5ef7b99e..25958315b1 100644 --- a/tests/tests-core/src/main/resources/graphql/node-tag-query +++ b/tests/tests-core/src/main/resources/graphql/node-tag-query @@ -2,9 +2,7 @@ node(path:"/Products/Concorde.en.html") { tags { elements { - # [$.data.node.tags.elements[0].name=Plane] - # [$.data.node.tags.elements[1].name=Twinjet] - # [$.data.node.tags.elements[2].name=red] + # [$.data.node.tags.elements[*].name=[Plane, Twinjet, red]] name } } diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/GraphQLHandler.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/GraphQLHandler.java index b5eb3e5dc7..692f4adbea 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/GraphQLHandler.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/GraphQLHandler.java @@ -107,6 +107,11 @@ public void handleQuery(GraphQLContext gc, String body) { dataLoaderRegistry.register(NodeDataLoader.PATH_LOADER_KEY, DataLoader.newDataLoader(NodeDataLoader.PATH_LOADER, dlOptions)); dataLoaderRegistry.register(NodeDataLoader.BREADCRUMB_LOADER_KEY, DataLoader.newDataLoader(NodeDataLoader.BREADCRUMB_LOADER, dlOptions)); dataLoaderRegistry.register(FieldDefinitionProvider.LINK_REPLACER_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().LINK_REPLACER_LOADER, dlOptions)); + dataLoaderRegistry.register(FieldDefinitionProvider.BOOLEAN_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().BOOLEAN_LIST_VALUE_LOADER, dlOptions)); + dataLoaderRegistry.register(FieldDefinitionProvider.DATE_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().DATE_LIST_VALUE_LOADER, dlOptions)); + dataLoaderRegistry.register(FieldDefinitionProvider.NUMBER_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().NUMBER_LIST_VALUE_LOADER, dlOptions)); + dataLoaderRegistry.register(FieldDefinitionProvider.HTML_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().HTML_LIST_VALUE_LOADER, dlOptions)); + dataLoaderRegistry.register(FieldDefinitionProvider.STRING_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().STRING_LIST_VALUE_LOADER, dlOptions)); ExecutionInput executionInput = ExecutionInput .newExecutionInput() diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/FieldDefinitionProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/FieldDefinitionProvider.java index 5d655e9f03..9dc4d86421 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/FieldDefinitionProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/FieldDefinitionProvider.java @@ -15,6 +15,7 @@ import static graphql.schema.GraphQLObjectType.newObject; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,7 +23,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletionStage; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -73,6 +76,7 @@ import com.gentics.mesh.graphql.type.NodeTypeProvider; import com.gentics.mesh.parameter.LinkType; import com.gentics.mesh.util.DateUtils; +import com.google.common.base.Functions; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; @@ -94,6 +98,31 @@ public class FieldDefinitionProvider extends AbstractTypeProvider { */ public static final String LINK_REPLACER_DATA_LOADER_KEY = "linkReplaceLoader"; + /** + * Key for the data loader for boolean list field values + */ + public static final String BOOLEAN_LIST_VALUES_DATA_LOADER_KEY = "booleanListLoader"; + + /** + * Key for the data loader for date list field values + */ + public static final String DATE_LIST_VALUES_DATA_LOADER_KEY = "dateListLoader"; + + /** + * Key for the data loader for number list field values + */ + public static final String NUMBER_LIST_VALUES_DATA_LOADER_KEY = "numberListLoader"; + + /** + * Key for the data loader for html list field values + */ + public static final String HTML_LIST_VALUES_DATA_LOADER_KEY = "htmlListLoader"; + + /** + * Key for the data loader for string list field values + */ + public static final String STRING_LIST_VALUES_DATA_LOADER_KEY = "stringListLoader"; + protected final MicronodeFieldTypeProvider micronodeFieldTypeProvider; protected final WebRootLinkReplacerImpl linkReplacer; @@ -117,6 +146,70 @@ private static void partitioningByLinkTypeAndText(List keys, BiCo */ public BatchLoaderWithContext LINK_REPLACER_LOADER; + /** + * Generic list field data loader + * @param type of the field values + * @param type fo the returned (rendered) field values + * @param keys list of listUuids for which the field values need to be loaded + * @param dataFunction function, that loads the list field values + * @param transformer transformer for transforming the loaded values into the returned values (e.g. for date formatting) + * @return CompletionStage for the lists of loaded list values + */ + private static CompletionStage>> listValueDataLoader(List keys, Function, Map>> dataFunction, Function>, List>> transformer) { + Promise>> promise = Promise.promise(); + + Map> listValuesMap = dataFunction.apply(keys); + List> valueLists = keys.stream().map(key -> listValuesMap.getOrDefault(key, Collections.emptyList())).collect(Collectors.toList()); + + promise.complete(transformer.apply(valueLists)); + return promise.future().toCompletionStage(); + } + + /** + * DataLoader implementation for values of boolean lists + */ + public BatchLoaderWithContext> BOOLEAN_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + return listValueDataLoader(keys, contentDao::getBooleanListFieldValues, Functions.identity()); + }; + + /** + * DataLoader implementation for values of date lists + */ + public BatchLoaderWithContext> DATE_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + + Function>, List>> dateFormatter = orig -> { + return orig.stream().map(origList -> origList.stream().map(date -> DateUtils.toISO8601(date, 0)).collect(Collectors.toList())).collect(Collectors.toList()); + }; + + return listValueDataLoader(keys, contentDao::getDateListFieldValues, dateFormatter); + }; + + /** + * DataLoader implementation for values of number lists + */ + public BatchLoaderWithContext> NUMBER_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + return listValueDataLoader(keys, contentDao::getNumberListFieldValues, Functions.identity()); + }; + + /** + * DataLoader implementation for values of string lists + */ + public BatchLoaderWithContext> HTML_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + return listValueDataLoader(keys, contentDao::getHtmlListFieldValues, Functions.identity()); + }; + + /** + * DataLoader implementation for values of string lists + */ + public BatchLoaderWithContext> STRING_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + return listValueDataLoader(keys, contentDao::getStringListFieldValues, Functions.identity()); + }; + @Inject public FieldDefinitionProvider(MeshOptions options, MicronodeFieldTypeProvider micronodeFieldTypeProvider, WebRootLinkReplacerImpl linkReplacer) { super(options); @@ -435,41 +528,98 @@ public GraphQLFieldDefinition createListDef(GraphQLContext context, ListFieldSch if (booleanList == null) { return null; } - return booleanList.getList().stream().map(item -> item.getBoolean()).collect(Collectors.toList()); + + String booleanListUuid = booleanList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(booleanListUuid)) { + DataLoader> booleanListValueLoader = env.getDataLoader(FieldDefinitionProvider.BOOLEAN_LIST_VALUES_DATA_LOADER_KEY); + return booleanListValueLoader.load(booleanListUuid); + } else { + return booleanList.getList().stream().map(item -> item.getBoolean()).collect(Collectors.toList()); + } case "html": HibHtmlFieldList htmlList = container.getHTMLList(schema.getName()); if (htmlList == null) { return null; } - return htmlList.getList().stream().map(item -> { - String content = item.getHTML(); + + String htmlListUuid = htmlList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(htmlListUuid)) { LinkType linkType = getLinkType(env); - return linkReplacer.replace(gc, null, null, content, linkType, tx.getProject(gc).getName(), - Arrays.asList(container.getLanguageTag())); - }).collect(Collectors.toList()); + DataLoader> htmlListValueLoader = env.getDataLoader(FieldDefinitionProvider.HTML_LIST_VALUES_DATA_LOADER_KEY); + + if (linkType != null && linkType != LinkType.OFF) { + String projectName = tx.getProject(gc).getName(); + List languageTags = Arrays.asList(container.getLanguageTag()); + return htmlListValueLoader.load(htmlListUuid).thenApply(contents -> { + return contents.stream().map(content -> linkReplacer.replace(gc, null, null, content, + linkType, projectName, languageTags)).collect(Collectors.toList()); + }); + } else { + return htmlListValueLoader.load(htmlListUuid); + } + } else { + return htmlList.getList().stream().map(item -> { + String content = item.getHTML(); + LinkType linkType = getLinkType(env); + return linkReplacer.replace(gc, null, null, content, linkType, tx.getProject(gc).getName(), + Arrays.asList(container.getLanguageTag())); + }).collect(Collectors.toList()); + } case "string": HibStringFieldList stringList = container.getStringList(schema.getName()); if (stringList == null) { return null; } - return stringList.getList().stream().map(item -> { - String content = item.getString(); + + String stringListUuid = stringList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(stringListUuid)) { LinkType linkType = getLinkType(env); - return linkReplacer.replace(gc, null, null, content, linkType, tx.getProject(gc).getName(), - Arrays.asList(container.getLanguageTag())); - }).collect(Collectors.toList()); + DataLoader> stringListValueLoader = env.getDataLoader(FieldDefinitionProvider.STRING_LIST_VALUES_DATA_LOADER_KEY); + + if (linkType != null && linkType != LinkType.OFF) { + String projectName = tx.getProject(gc).getName(); + List languageTags = Arrays.asList(container.getLanguageTag()); + return stringListValueLoader.load(stringListUuid).thenApply(contents -> { + return contents.stream().map(content -> linkReplacer.replace(gc, null, null, content, + linkType, projectName, languageTags)).collect(Collectors.toList()); + }); + } else { + return stringListValueLoader.load(stringListUuid); + } + } else { + return stringList.getList().stream().map(item -> { + String content = item.getString(); + LinkType linkType = getLinkType(env); + return linkReplacer.replace(gc, null, null, content, linkType, tx.getProject(gc).getName(), + Arrays.asList(container.getLanguageTag())); + }).collect(Collectors.toList()); + } case "number": HibNumberFieldList numberList = container.getNumberList(schema.getName()); if (numberList == null) { return null; } - return numberList.getList().stream().map(item -> item.getNumber()).collect(Collectors.toList()); + + String numberListUuid = numberList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(numberListUuid)) { + DataLoader> numberListValueLoader = env.getDataLoader(FieldDefinitionProvider.NUMBER_LIST_VALUES_DATA_LOADER_KEY); + return numberListValueLoader.load(numberListUuid); + } else { + return numberList.getList().stream().map(item -> item.getNumber()).collect(Collectors.toList()); + } case "date": HibDateFieldList dateList = container.getDateList(schema.getName()); if (dateList == null) { return null; } - return dateList.getList().stream().map(item -> DateUtils.toISO8601(item.getDate(), 0)).collect(Collectors.toList()); + + String dateListUuid = dateList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(dateListUuid)) { + DataLoader> dateListValueLoader = env.getDataLoader(FieldDefinitionProvider.DATE_LIST_VALUES_DATA_LOADER_KEY); + return dateListValueLoader.load(dateListUuid); + } else { + return dateList.getList().stream().map(item -> DateUtils.toISO8601(item.getDate(), 0)).collect(Collectors.toList()); + } case "node": HibNodeFieldList nodeList = container.getNodeList(schema.getName()); if (nodeList == null) {