diff --git a/bom/pom.xml b/bom/pom.xml index c97c5e7888..4926bdd1e0 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -17,11 +17,12 @@ UTF-8 - + 21.5 21.0 3.2.2 - 3.0.7 + 3.0.8 3.14.1 3.3.1 6.3.5 @@ -49,7 +50,8 @@ 1.15.5 - + @@ -183,11 +185,11 @@ org.slf4j - slf4j-api + slf4j-api org.slf4j - jcl-over-slf4j + jcl-over-slf4j @@ -198,11 +200,11 @@ org.slf4j - slf4j-api + slf4j-api org.slf4j - jcl-over-slf4j + jcl-over-slf4j @@ -270,7 +272,7 @@ com.jayway.jsonpath json-path - 2.9.0 + 2.10.0 org.skyscreamer @@ -283,7 +285,7 @@ 2.7.1 - org.testcontainers + org.testcontainers testcontainers ${testcontainers.version} @@ -381,6 +383,11 @@ vertx-hazelcast ${vertx.version} + + io.vertx + vertx-json-schema + ${vertx.version} + io.vertx vertx-auth-common @@ -808,9 +815,9 @@ 3.2.0 - jakarta.persistence - jakarta.persistence-api - 3.2.0 + jakarta.persistence + jakarta.persistence-api + 3.2.0 com.squareup.inject @@ -833,9 +840,9 @@ ${hazelcast-hibernate.version} - org.hibernate.orm - hibernate-jcache - ${hibernate.version} + org.hibernate.orm + hibernate-jcache + ${hibernate.version} org.hibernate.orm diff --git a/changelog/src/changelog/entries/2026/04/8741.GPU-2210.enhancement b/changelog/src/changelog/entries/2026/04/8741.GPU-2210.enhancement new file mode 100644 index 0000000000..a1649553ad --- /dev/null +++ b/changelog/src/changelog/entries/2026/04/8741.GPU-2210.enhancement @@ -0,0 +1 @@ +Core: A new field type, `JSON`, has been added. Having a string in its base, it provides such additional functionality, as JSON structure / schema verification, and extended JsonPath filtering in GraphQL requests. \ No newline at end of file diff --git a/common/src/main/resources/i18n/translations_de.properties b/common/src/main/resources/i18n/translations_de.properties index a3e4950c55..766652b9c7 100644 --- a/common/src/main/resources/i18n/translations_de.properties +++ b/common/src/main/resources/i18n/translations_de.properties @@ -179,6 +179,7 @@ node_error_invalid_microschema_field_value=Der Wert für das Feld "{0}" darf nic node_list_item_not_found=Der in der Liste angegebene Node mit der uuid {0} konnte nicht gefunden werden. node_update_failed=Aktualisierung des Node "{0}" ist fehlgeschlagen. node_error_invalid_string_field_value=Das String Feld "{0}" darf nicht mit dem Wert "{1}" befüllt werden. +node_error_invalid_json_field_value=Das JSON Feld "{0}" darf nicht mit dem Wert "{1}" befüllt werden. node_error_string_field_value_too_long=Das String Feld "{0}" kann höchstens {1} Zeichen enthalten, übergeben wurden {2} Zeichen. node_conflicting_segmentfield_update=Das Segmentfeld "{0}" kann nicht mit dem Wert "{1}" befüllt werden, weil dieser Wert bereits verwendet wird. node_conflicting_segmentfield_upload=Die Datei "{1}" kann nicht in das Segmentfeld "{0}" geladen werden, weil der Dateiname bereits verwendet wird. diff --git a/common/src/main/resources/i18n/translations_en.properties b/common/src/main/resources/i18n/translations_en.properties index 784e3f577d..a2ed07210b 100644 --- a/common/src/main/resources/i18n/translations_en.properties +++ b/common/src/main/resources/i18n/translations_en.properties @@ -178,6 +178,7 @@ node_error_invalid_schema_field_value=The field "{0}" is not allowed to use sche node_list_item_not_found=Node within node list with uuid {0} could not be found. node_update_failed=Update of node "{0}" failed. node_error_invalid_string_field_value=The string field "{0}" must not be set to value "{1}". +node_error_invalid_json_field_value=The JSON field "{0}" must not be set to value "{1}". node_error_string_field_value_too_long=The string Feld "{0}" can only hold up to {1} characters, but {2} characters were given. node_conflicting_segmentfield_update=The segment field "{0}" must not be set to value "{1}" because this value is already used. node_conflicting_segmentfield_upload=The file "{1}" cannot be uploaded into the segment field "{0}" because the filename is already in use. diff --git a/common/src/main/resources/i18n/translations_zh.properties b/common/src/main/resources/i18n/translations_zh.properties index a78c3f5a21..7811ab55ca 100644 --- a/common/src/main/resources/i18n/translations_zh.properties +++ b/common/src/main/resources/i18n/translations_zh.properties @@ -169,6 +169,7 @@ node_error_invalid_microschema_field_value=字段“{0}”的值不得使用内 node_list_item_not_found=在uuid为{0}的节点列表中找不到节点。 node_update_failed=节点“{0}”更新失败。 node_error_invalid_string_field_value=字符串字段“{0}”不得设置为值“{1}”。 +node_error_invalid_json_field_value=JSON 字段“{0}”不能设置为值“{1}”。 node_conflicting_segmentfield_update=分节字段“{0}”不得设置为值“{1}”,因为该值已被使用。 node_conflicting_segmentfield_upload=文件“{1}”无法上传到分节字段“{0}”中,因为文件名已被使用。 node_conflicting_segmentfield_move=无法移动节点,因为分节字段“{0}”中的值“{1}”发生冲突。 diff --git a/connectors/common/src/main/java/com/gentics/mesh/database/connector/AbstractDatabaseConnector.java b/connectors/common/src/main/java/com/gentics/mesh/database/connector/AbstractDatabaseConnector.java index 3cce115ba6..bb17f05dc0 100644 --- a/connectors/common/src/main/java/com/gentics/mesh/database/connector/AbstractDatabaseConnector.java +++ b/connectors/common/src/main/java/com/gentics/mesh/database/connector/AbstractDatabaseConnector.java @@ -37,6 +37,7 @@ import org.hibernate.query.Query; import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.spi.Limit; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; import org.slf4j.Logger; @@ -46,6 +47,7 @@ import com.gentics.mesh.core.data.project.HibProject; import com.gentics.mesh.core.data.schema.HibFieldSchemaVersionElement; import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.database.HibernateTx; import com.gentics.mesh.etc.config.HibernateMeshOptions; import com.gentics.mesh.hibernate.MeshTablePrefixStrategy; @@ -385,6 +387,11 @@ public String getSqlTypeName(FieldTypes type, String uuidTypeName) { TypeConfiguration typeConfig = getSessionMetadataIntegrator().getSessionFactoryImplementor().getTypeConfiguration(); Dialect dialect = getHibernateDialect(); switch (type) { + case JSON: + return typeConfig.getDdlTypeRegistry().getTypeName( + SqlTypes.JSON, + new Size(getSqlTypePrecision(type), getSqlTypeScale(type), getSqlTypeLength(type)), + typeConfig.getBasicTypeForJavaType(JsonContent.class)); case STRING: case HTML: return typeConfig.getDdlTypeRegistry().getTypeName( diff --git a/connectors/common/src/main/resources/META-INF/liquibase/changelog-master.xml b/connectors/common/src/main/resources/META-INF/liquibase/changelog-master.xml index 156e01d2e4..d854a653f2 100644 --- a/connectors/common/src/main/resources/META-INF/liquibase/changelog-master.xml +++ b/connectors/common/src/main/resources/META-INF/liquibase/changelog-master.xml @@ -25,4 +25,6 @@ + + diff --git a/connectors/common/src/main/resources/META-INF/liquibase/entries-3.3.x/1-features/changelog-gpu-2210.xml b/connectors/common/src/main/resources/META-INF/liquibase/entries-3.3.x/1-features/changelog-gpu-2210.xml new file mode 100644 index 0000000000..b3331f3706 --- /dev/null +++ b/connectors/common/src/main/resources/META-INF/liquibase/entries-3.3.x/1-features/changelog-gpu-2210.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/connectors/hsqldb/src/main/java/com/gentics/mesh/database/connector/HSQLDBConnector.java b/connectors/hsqldb/src/main/java/com/gentics/mesh/database/connector/HSQLDBConnector.java index c618e45ce6..65341deb5f 100644 --- a/connectors/hsqldb/src/main/java/com/gentics/mesh/database/connector/HSQLDBConnector.java +++ b/connectors/hsqldb/src/main/java/com/gentics/mesh/database/connector/HSQLDBConnector.java @@ -3,10 +3,10 @@ import java.util.Map; import org.apache.commons.lang3.StringUtils; -import org.hibernate.dialect.HSQLDialect; import org.hsqldb.jdbcDriver; import com.gentics.mesh.etc.config.HibernateMeshOptions; +import com.gentics.mesh.hibernate.dialect.HSQLJsonAwareDialect; /** * HSQLDB Mesh database connector @@ -47,8 +47,7 @@ protected String getDefaultDriverClassName() { @Override protected String getDefaultDialectClassName() { - // TODO Auto-generated method stub - return HSQLDialect.class.getCanonicalName(); + return HSQLJsonAwareDialect.class.getCanonicalName(); } @Override diff --git a/connectors/hsqldb/src/main/java/com/gentics/mesh/hibernate/dialect/HSQLJsonAwareDialect.java b/connectors/hsqldb/src/main/java/com/gentics/mesh/hibernate/dialect/HSQLJsonAwareDialect.java new file mode 100644 index 0000000000..1d9ac22988 --- /dev/null +++ b/connectors/hsqldb/src/main/java/com/gentics/mesh/hibernate/dialect/HSQLJsonAwareDialect.java @@ -0,0 +1,43 @@ +package com.gentics.mesh.hibernate.dialect; + +import static org.hibernate.type.SqlTypes.JSON; + +import java.sql.Types; + +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JsonJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +import com.gentics.mesh.database.connector.QueryUtils; + +/** + * JSON object supporting (via VARCHAR) dialect of HSQLDB. + */ +public class HSQLJsonAwareDialect extends HSQLDialect { + + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes( typeContributions, serviceRegistry ); + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "varchar(" + QueryUtils.DEFAULT_STRING_LENGTH + ")", this ) ); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE ); + super.contributeTypes( typeContributions, serviceRegistry ); + } + + @Override + public boolean equivalentTypes(int typeCode1, int typeCode2) { + return typeCode1 == Types.LONGVARCHAR && typeCode2 == SqlTypes.JSON + || typeCode1 == SqlTypes.JSON && typeCode2 == Types.LONGVARCHAR + || super.equivalentTypes( typeCode1, typeCode2 ); + } +} diff --git a/core/src/main/java/com/gentics/mesh/core/endpoint/webrootfield/WebRootFieldHandler.java b/core/src/main/java/com/gentics/mesh/core/endpoint/webrootfield/WebRootFieldHandler.java index 03d31ab656..3cf6a18e80 100644 --- a/core/src/main/java/com/gentics/mesh/core/endpoint/webrootfield/WebRootFieldHandler.java +++ b/core/src/main/java/com/gentics/mesh/core/endpoint/webrootfield/WebRootFieldHandler.java @@ -145,7 +145,7 @@ public void handleGetPathField(RoutingContext rc) { // Check if field is JSON, to set corresponding HTTP header value String contentType = rc.response().headers().get(HttpHeaders.CONTENT_TYPE); ac.send( - HttpConstants.APPLICATION_JSON_UTF8.equals(contentType) ? field.toJson(ac.isMinify(options.getHttpServerOptions())) : field.toString(), + (HttpConstants.APPLICATION_JSON_UTF8.equals(contentType) && FieldTypes.valueByName(field.getType()) != FieldTypes.JSON) ? field.toJson(ac.isMinify(options.getHttpServerOptions())) : field.toString(), HttpResponseStatus.valueOf(rc.response().getStatusCode()), contentType); } diff --git a/doc/src/main/hugo/content/docs/building-blocks.asciidoc b/doc/src/main/hugo/content/docs/building-blocks.asciidoc index 09aa1918c5..f7276fb72b 100644 --- a/doc/src/main/hugo/content/docs/building-blocks.asciidoc +++ b/doc/src/main/hugo/content/docs/building-blocks.asciidoc @@ -143,7 +143,7 @@ include::content/docs/api/response{apiLatest}/\{project\}/nodes/\{nodeUuid\}/200 == Schema -Typically, each project will require a set of different content types. Together they can be considered the content model of your project. Staying with the example of a product catalogue website: a product, product category, product image, and product manual each represent a separate content type. In Gentics Mesh, a *schema* is used to define such content types in terms of a couple of standard fields (e.g. ```uuid```, ```name```, ```description```, ```version```, etc.) and an arbitrary number of custom fields. Available <> are ```string```, ```number```, ```HTML```, ```date```, ```binary```, ```list```, ```node```, ```micronode```, ```boolean```. You can think of a schema as a blueprint for new content items. +Typically, each project will require a set of different content types. Together they can be considered the content model of your project. Staying with the example of a product catalogue website: a product, product category, product image, and product manual each represent a separate content type. In Gentics Mesh, a *schema* is used to define such content types in terms of a couple of standard fields (e.g. ```uuid```, ```name```, ```description```, ```version```, etc.) and an arbitrary number of custom fields. Available <> are ```string```, ```number```, ```HTML```, ```date```, ```binary```, ```list```, ```node```, ```micronode```, ```boolean```, ```json```. You can think of a schema as a blueprint for new content items. TIP: Using the ```container``` property, a schema can be configured to allow for hierarchically structuring nodes. Nodes based on a such a schema may contain child nodes. This is the basis for building link:{{< relref "features.asciidoc" >}}#_contenttrees[content trees] in Gentics Mesh and leveraging the power of automatic link:{{< relref "features.asciidoc" >}}#_navigation[navigation menus], link:{{< relref "features.asciidoc" >}}#_breadcrumbs[breadcrumbs] and link:{{< relref "features.asciidoc" >}}#_prettyurls[pretty URLs]. @@ -287,6 +287,12 @@ The ```html``` field type stores HTML data. The ```required``` property indicates if the field is mandatory or not. +===== JSON +The ```json``` field type stores JSON data. +The ```required``` property indicates if the field is mandatory or not. +The optional ```allow``` property acts as a whitelist for allowed field schemas. + + ===== Micronode A ```micronode``` field type stores a single micronode. A micronode is similar to a node. Typically they do not exist on their own but are tied to their (parent) node, e.g. a caption to be used in a image node. For a detailed description see our definition of <>. The ```required``` property indicates if the field is mandatory or not. @@ -302,7 +308,7 @@ A ```node``` field type must have an ```allow``` property, which acts as a white ===== List A ```list``` field type allows for specifying a list with elements on the basis of other field types and thus represents a powerful mechanism for building your content model: -(1) Within a node you can have simple lists of arbitrary length. The ```listType``` property then has to be of type ```string```, ```number```, ```date```, ```boolean```, or ```HTML```. E.g. handling your recipe nodes of your food blog will be a breeze with string-typed lists for ingredients. +(1) Within a node you can have simple lists of arbitrary length. The ```listType``` property then has to be of type ```string```, ```number```, ```date```, ```boolean```, ```json```, or ```HTML```. E.g. handling your recipe nodes of your food blog will be a breeze with string-typed lists for ingredients. (2) You can unleash the power of micronodes, by specifying a list with the ```listType``` property set to ```micronode```, and the ```allow``` property set to the allowed microschemas. For example, besides having title, teaser, date and author fields, your blog post schema could define a content field of type list allowing to insert any of your microschemas (e.g. YouTube Video, Image, Text, Galleries, Google Maps, etc.). @@ -399,9 +405,9 @@ Microschemas share the same properties as schemas except for the properties ```d === Schema Field Types -In comparison to nodes, micronodes can be built with schema field types ```String```, ```Number```, ```Date```, ```Boolean```, ```HTML```, ```Node```, and ```Lists```. +In comparison to nodes, micronodes can be built with schema field types ```String```, ```Number```, ```Date```, ```Boolean```, ```JSON```, ```HTML```, ```Node```, and ```Lists```. -Fields of type ```list``` in micronodes can be of type: ```String```, ```Number```, ```Date```, ```Boolean```, ```HTML```, and ```Node```. +Fields of type ```list``` in micronodes can be of type: ```String```, ```Number```, ```Date```, ```Boolean```,```JSON``` , ```HTML```, and ```Node```. == Tag diff --git a/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerMappingProviderImpl.java b/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerMappingProviderImpl.java index 3664bde3af..702f72fa5d 100644 --- a/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerMappingProviderImpl.java +++ b/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerMappingProviderImpl.java @@ -192,6 +192,9 @@ public Optional getFieldMapping(FieldSchema fieldSchema, HibBranch b case BOOLEAN: addBooleanFieldMapping(fieldInfo, customIndexOptions); break; + case JSON: + addJsonFieldMapping(fieldInfo, customIndexOptions); + break; case DATE: addDataFieldMapping(fieldInfo, customIndexOptions); break; @@ -233,6 +236,14 @@ private void addBooleanFieldMapping(JsonObject fieldInfo, JsonObject customIndex } } + private void addJsonFieldMapping(JsonObject fieldInfo, JsonObject customIndexOptions) { + if (isStrictMode()) { + fieldInfo.mergeIn(customIndexOptions); + } else { + fieldInfo.put("type", OBJECT); + } + } + private void addDataFieldMapping(JsonObject fieldInfo, JsonObject customIndexOptions) { if (isStrictMode()) { fieldInfo.mergeIn(customIndexOptions); @@ -282,21 +293,21 @@ private void addListFieldMapping(JsonObject fieldInfo, HibBranch branch, ListFie } else { ListFieldSchemaImpl listFieldSchema = (ListFieldSchemaImpl) fieldSchema; String type = listFieldSchema.getListType(); - switch (type) { - case "node": + switch (FieldTypes.valueByName(type)) { + case NODE: fieldInfo.put("type", KEYWORD); fieldInfo.put("index", INDEX_VALUE); break; - case "date": + case DATE: fieldInfo.put("type", DATE); break; - case "number": + case NUMBER: fieldInfo.put("type", DOUBLE); break; - case "boolean": + case BOOLEAN: fieldInfo.put("type", BOOLEAN); break; - case "micronode": + case MICRONODE: fieldInfo.put("type", NESTED); // All allowed microschemas @@ -307,13 +318,19 @@ private void addListFieldMapping(JsonObject fieldInfo, HibBranch branch, ListFie // fieldProps.put(field.getName(), fieldInfo); break; - case "string": - case "html": + case STRING: + case HTML: fieldInfo.put("type", TEXT); if (customIndexOptions != null) { fieldInfo.put("fields", customIndexOptions); } break; + case JSON: + fieldInfo.put("type", OBJECT); + if (customIndexOptions != null) { + fieldInfo.put("fields", customIndexOptions); + } + break; default: throw new RuntimeException("Unknown mapping type for the field type {" + type + "}."); } diff --git a/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerTransformer.java b/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerTransformer.java index 18dab69b6a..523afe4e16 100644 --- a/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerTransformer.java +++ b/elasticsearch/src/main/java/com/gentics/mesh/search/index/node/NodeContainerTransformer.java @@ -41,11 +41,13 @@ import com.gentics.mesh.core.data.node.field.HibDateField; import com.gentics.mesh.core.data.node.field.HibHtmlField; import com.gentics.mesh.core.data.node.field.HibImageDataField; +import com.gentics.mesh.core.data.node.field.HibJsonField; import com.gentics.mesh.core.data.node.field.HibNumberField; import com.gentics.mesh.core.data.node.field.HibStringField; import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -62,6 +64,7 @@ import com.gentics.mesh.core.db.Tx; import com.gentics.mesh.core.rest.common.ContainerType; import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.binary.BinaryMetadata; import com.gentics.mesh.core.rest.node.field.binary.Location; import com.gentics.mesh.core.rest.schema.BinaryExtractOptions; @@ -236,6 +239,12 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(name, booleanField.getBoolean()); } break; + case JSON: + HibJsonField jsonField = container.getJson(name); + if (jsonField != null) { + fieldsMap.put(name, jsonField.getJson()); + } + break; case DATE: HibDateField dateField = container.getDate(name); if (dateField != null) { @@ -262,8 +271,8 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co case LIST: if (fieldSchema instanceof ListFieldSchemaImpl) { ListFieldSchemaImpl listFieldSchema = (ListFieldSchemaImpl) fieldSchema; - switch (listFieldSchema.getListType()) { - case "node": + switch (FieldTypes.valueByName(listFieldSchema.getListType())) { + case NODE: HibNodeFieldList graphNodeList = container.getNodeList(fieldSchema.getName()); if (graphNodeList != null) { List nodeItems = new ArrayList<>(); @@ -276,7 +285,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), nodeItems); } break; - case "date": + case DATE: HibDateFieldList graphDateList = container.getDateList(fieldSchema.getName()); if (graphDateList != null) { List dateItems = new ArrayList<>(); @@ -286,7 +295,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), dateItems); } break; - case "number": + case NUMBER: HibNumberFieldList graphNumberList = container.getNumberList(fieldSchema.getName()); if (graphNumberList != null) { List numberItems = new ArrayList<>(); @@ -298,7 +307,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), numberItems); } break; - case "boolean": + case BOOLEAN: HibBooleanFieldList graphBooleanList = container.getBooleanList(fieldSchema.getName()); if (graphBooleanList != null) { List booleanItems = new ArrayList<>(); @@ -308,7 +317,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), booleanItems); } break; - case "micronode": + case MICRONODE: HibMicronodeFieldList micronodeGraphFieldList = container.getMicronodeList(fieldSchema.getName()); if (micronodeGraphFieldList != null) { // Add list of micronode objects @@ -323,7 +332,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co }).toList().blockingGet()); } break; - case "string": + case STRING: HibStringFieldList graphStringList = container.getStringList(fieldSchema.getName()); if (graphStringList != null) { List stringItems = new ArrayList<>(); @@ -337,7 +346,7 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), stringItems); } break; - case "html": + case HTML: HibHtmlFieldList graphHtmlList = container.getHTMLList(fieldSchema.getName()); if (graphHtmlList != null) { List htmlItems = new ArrayList<>(); @@ -356,6 +365,16 @@ public void addFields(JsonObject document, String fieldKey, HibFieldContainer co fieldsMap.put(fieldSchema.getName(), htmlItems); } break; + case JSON: + HibJsonFieldList sqlJsonList = container.getJsonList(fieldSchema.getName()); + if (sqlJsonList != null) { + List jsonItems = new ArrayList<>(); + for (HibJsonField listItem : sqlJsonList.getList()) { + jsonItems.add(listItem.getJson()); + } + fieldsMap.put(fieldSchema.getName(), jsonItems); + } + break; default: log.error("Unknown list type {" + listFieldSchema.getListType() + "}"); break; diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/HibFieldContainer.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/HibFieldContainer.java index 6b704f7ab5..3e763962ec 100644 --- a/mdm/api/src/main/java/com/gentics/mesh/core/data/HibFieldContainer.java +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/HibFieldContainer.java @@ -22,11 +22,13 @@ import com.gentics.mesh.core.data.node.field.HibBooleanField; import com.gentics.mesh.core.data.node.field.HibDateField; import com.gentics.mesh.core.data.node.field.HibHtmlField; +import com.gentics.mesh.core.data.node.field.HibJsonField; import com.gentics.mesh.core.data.node.field.HibNumberField; import com.gentics.mesh.core.data.node.field.HibStringField; import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -250,6 +252,14 @@ default Stream getContents() { */ HibStringField getString(String key); + /** + * Return the JSON object field for the given key. + * + * @param key + * @return + */ + HibJsonField getJson(String key); + /** * Return the binary field for the given key. * @@ -354,6 +364,14 @@ default Stream getContents() { */ HibNumberFieldList getNumberList(String fieldKey); + /** + * Return JSOB object list. + * + * @param fieldKey + * @return + */ + HibJsonFieldList getJsonList(String fieldKey); + /** * Return node list. * @@ -426,6 +444,14 @@ default Stream getContents() { */ HibNumberFieldList createNumberList(String fieldKey); + /** + * Create a new JSON object list. + * + * @param fieldKey + * @return + */ + HibJsonFieldList createJsonList(String fieldKey); + /** * Create a new html list. * @@ -492,6 +518,14 @@ default Stream getContents() { */ HibDateField createDate(String key); + /** + * Create a new JSON object field. + * + * @param key + * @return + */ + HibJsonField createJson(String key); + /** * Create a new node field. * 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 712626f4c7..2358f3e585 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 @@ -37,6 +37,7 @@ import com.gentics.mesh.core.rest.error.Errors; import com.gentics.mesh.core.rest.event.node.NodeMeshEventModel; import com.gentics.mesh.core.rest.node.FieldMap; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.NodeFieldListItem; import com.gentics.mesh.core.rest.node.version.VersionInfo; import com.gentics.mesh.core.rest.schema.SchemaModel; @@ -1102,6 +1103,13 @@ default Result getFieldEdges(HibNode fieldN */ boolean supportsPrefetchingListFieldValues(); + /** + * Get the JSON object list field values for the given list UUIDs + * @param listUuids list UUIDs + * @return map of list UUIDs to lists of JSON values + */ + Map> getJsonListFieldValues(List listUuids); + /** * Get the boolean list field values for the given list UUIDs * @param listUuids list UUIDs diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/HibJsonField.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/HibJsonField.java new file mode 100644 index 0000000000..bd3196f9b6 --- /dev/null +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/HibJsonField.java @@ -0,0 +1,55 @@ +package com.gentics.mesh.core.data.node.field; + +import com.gentics.mesh.core.data.HibField; +import com.gentics.mesh.core.data.HibFieldContainer; +import com.gentics.mesh.core.data.node.field.nesting.HibListableField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; +import com.gentics.mesh.handler.ActionContext; +import com.gentics.mesh.util.CompareUtils; + +public interface HibJsonField extends HibListableField, HibBasicField { + + /** + * Set the JSON object within the field. + * + * @param json + */ + void setJson(JsonContent json); + + /** + * Return the JSON object which is stored in the field. + * + * @return + */ + JsonContent getJson(); + + @Override + default HibField cloneTo(HibFieldContainer container) { + HibJsonField clone = container.createJson(getFieldKey()); + clone.setJson(getJson()); + return clone; + } + + @Override + default JsonField transformToRest(ActionContext ac) { + JsonField jsonField = new JsonFieldImpl(); + jsonField.setJson(getJson()); + return jsonField; + } + + default boolean jsonEquals(Object obj) { + if (obj instanceof HibJsonField) { + JsonContent jsonA = getJson(); + JsonContent jsonB = ((HibJsonField) obj).getJson(); + return CompareUtils.equals(jsonA, jsonB); + } + if (obj instanceof JsonField) { + JsonContent jsonA = getJson(); + JsonContent jsonB = ((JsonField) obj).getJson(); + return CompareUtils.equals(jsonA, jsonB); + } + return false; + } +} diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/list/HibJsonFieldList.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/list/HibJsonFieldList.java new file mode 100644 index 0000000000..2fec6fd62e --- /dev/null +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/field/list/HibJsonFieldList.java @@ -0,0 +1,70 @@ +package com.gentics.mesh.core.data.node.field.list; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.gentics.mesh.context.InternalActionContext; +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.data.node.field.nesting.HibMicroschemaListableField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; +import com.gentics.mesh.json.JsonUtil; +import com.gentics.mesh.util.CompareUtils; + +public interface HibJsonFieldList extends HibMicroschemaListableField, HibListField { + + String TYPE = "json"; + + /** + * Add another sql field to the list of sql fields. + * + * @param json + * Json to be set for the new field + * @return + */ + HibJsonField createJson(JsonContent json); + + /** + * Create an ordered list of json fields from values, adding all to the list. + * + * @param jsons + */ + default void createJsons(List jsons) { + jsons.stream().forEach(this::createJson); + } + + /** + * Return the json field at the given index of the list. + * + * @param index + * @return + */ + HibJsonField getJson(int index); + + @Override + default JsonFieldListImpl transformToRest(InternalActionContext ac, String fieldKey, List languageTags, int level) { + JsonFieldListImpl restModel = new JsonFieldListImpl(); + for (HibJsonField item : getList()) { + restModel.add(item.getJson()); + } + return restModel; + } + + @Override + default List getValues() { + return getList().stream().map(HibJsonField::getJson).collect(Collectors.toList()); + } + + @Override + default boolean listEquals(Object obj) { + if (obj instanceof JsonFieldListImpl) { + JsonFieldListImpl restField = (JsonFieldListImpl) obj; + List restList = restField.getItems(); + List sqlList = getList(); + List valueList = sqlList.stream().map(e -> e.getJson()).collect(Collectors.toList()); + return CompareUtils.equals(restList, valueList, Optional.of((a, b) -> JsonUtil.COMPARATOR.compare(a, b) == 0)); + } + return HibListField.super.listEquals(obj); + } +} diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/node/handler/TypeConverter.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/handler/TypeConverter.java index 64d16262d0..80f648a7ff 100644 --- a/mdm/api/src/main/java/com/gentics/mesh/core/data/node/handler/TypeConverter.java +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/node/handler/TypeConverter.java @@ -19,6 +19,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.ListField; import com.gentics.mesh.core.rest.node.field.MicronodeField; import com.gentics.mesh.core.rest.node.field.NodeField; @@ -31,15 +35,13 @@ import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.MicronodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListItemImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.StringFieldListImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Type converter for the script engine used by the node migration handler. */ @@ -84,6 +86,17 @@ public StringFieldListImpl toStringList(Object value) { return listField(StringFieldListImpl::new, this::toString, value); } + /** + * Convert the given value to a string list + * + * @param value + * Value to be converted + * @return String array + */ + public JsonFieldListImpl toJsonList(Object value) { + return listField(JsonFieldListImpl::new, this::toJsonContent, value); + } + /** * Convert the given value to an HTML list * @@ -118,6 +131,29 @@ public Boolean toBoolean(Object value) { } } + /** + * Convert the given value to a boolean. + * + * @param value + * Value to be converted + * @return Boolean value + */ + public JsonContent toJsonContent(Object value) { + value = firstIfList(value); + + if (value == null) { + return null; + } + try { + return JsonContent.fromString(value.toString()); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Could not convert to JsonObject {" + value.toString() + "}", e); + } + } + return null; + } + /** * Convert the given value to a boolean array. * diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibAddFieldChange.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibAddFieldChange.java index d733716778..235497be83 100644 --- a/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibAddFieldChange.java +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibAddFieldChange.java @@ -1,27 +1,33 @@ package com.gentics.mesh.core.data.schema; import static com.gentics.mesh.core.rest.error.Errors.error; -import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.*; +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.ADD_FIELD_AFTER_KEY; +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.ALLOW_KEY; +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.LIST_TYPE_KEY; +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.TYPE_KEY; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.stream.Stream; -import com.gentics.mesh.core.rest.schema.*; -import com.gentics.mesh.core.rest.schema.impl.*; -import io.vertx.core.json.JsonObject; import org.apache.commons.lang3.BooleanUtils; +import com.gentics.mesh.core.rest.JsonSchema; import com.gentics.mesh.core.rest.common.FieldContainer; +import com.gentics.mesh.core.rest.common.FieldTypes; import com.gentics.mesh.core.rest.node.field.Field; import com.gentics.mesh.core.rest.schema.BinaryExtractOptions; import com.gentics.mesh.core.rest.schema.BinaryFieldSchema; import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.FieldSchemaContainer; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; import com.gentics.mesh.core.rest.schema.NodeFieldSchema; +import com.gentics.mesh.core.rest.schema.S3BinaryExtractOptions; +import com.gentics.mesh.core.rest.schema.S3BinaryFieldSchema; import com.gentics.mesh.core.rest.schema.StringFieldSchema; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeOperation; @@ -29,12 +35,16 @@ import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NumberFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.S3BinaryFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.StringFieldSchemaImpl; +import io.vertx.core.json.JsonObject; + /** * Change entry which contains information for a field to be added to the schema. */ @@ -159,19 +169,24 @@ default R apply(R container) { String position = getInsertAfterPosition(); FieldSchema field = null; // TODO avoid case switches like this. We need a central delegator implementation which will be used in multiple places - switch (getType()) { - case "html": + switch (FieldTypes.valueByName(getType())) { + case JSON: + JsonFieldSchema jsonField = new JsonFieldSchemaImpl(); + jsonField.setAllowedSchemas(getAllowProp() != null ? Arrays.stream(getAllowProp()).map(JsonSchema::new).toArray(size -> new JsonSchema[size]) : null); + field = jsonField; + break; + case HTML: field = new HtmlFieldSchemaImpl(); break; - case "string": + case STRING: StringFieldSchema stringField = new StringFieldSchemaImpl(); stringField.setAllowedValues(getAllowProp()); field = stringField; break; - case "number": + case NUMBER: field = new NumberFieldSchemaImpl(); break; - case "binary": + case BINARY: BinaryFieldSchema binaryField = new BinaryFieldSchemaImpl(); Boolean content = getRestProperty(BinaryFieldSchemaImpl.CHANGE_EXTRACT_CONTENT_KEY); Boolean metadata = getRestProperty(BinaryFieldSchemaImpl.CHANGE_EXTRACT_METADATA_KEY); @@ -183,7 +198,7 @@ default R apply(R container) { } field = binaryField; break; - case "s3binary": + case S3BINARY: S3BinaryFieldSchema s3binaryField = new S3BinaryFieldSchemaImpl(); Boolean s3Content = getRestProperty(S3BinaryFieldSchemaImpl.CHANGE_EXTRACT_CONTENT_KEY); Boolean s3Metadata = getRestProperty(S3BinaryFieldSchemaImpl.CHANGE_EXTRACT_METADATA_KEY); @@ -195,31 +210,34 @@ default R apply(R container) { } field = s3binaryField; break; - case "node": + case NODE: NodeFieldSchema nodeField = new NodeFieldSchemaImpl(); nodeField.setAllowedSchemas(getAllowProp()); field = nodeField; break; - case "micronode": + case MICRONODE: MicronodeFieldSchema micronodeFieldSchema = new MicronodeFieldSchemaImpl(); micronodeFieldSchema.setAllowedMicroSchemas(getAllowProp()); field = micronodeFieldSchema; break; - case "date": + case DATE: field = new DateFieldSchemaImpl(); break; - case "boolean": + case BOOLEAN: field = new BooleanFieldSchemaImpl(); break; - case "list": + case LIST: ListFieldSchema listField = new ListFieldSchemaImpl(); listField.setListType(getListType()); field = listField; - switch (getListType()) { - case "node": - case "micronode": + switch (FieldTypes.valueByName(getListType())) { + case NODE: + case MICRONODE: + case JSON: listField.setAllowedSchemas(getAllowProp()); break; + default: + break; } break; default: diff --git a/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibFieldTypeChange.java b/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibFieldTypeChange.java index 820573811c..cadc59be2d 100644 --- a/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibFieldTypeChange.java +++ b/mdm/api/src/main/java/com/gentics/mesh/core/data/schema/HibFieldTypeChange.java @@ -3,28 +3,31 @@ import static com.gentics.mesh.core.rest.error.Errors.error; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; -import java.util.*; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.function.Supplier; import com.gentics.mesh.core.data.node.handler.TypeConverter; import com.gentics.mesh.core.rest.common.FieldContainer; -import com.gentics.mesh.core.rest.node.field.*; +import com.gentics.mesh.core.rest.common.FieldTypes; import com.gentics.mesh.core.rest.node.field.BinaryField; import com.gentics.mesh.core.rest.node.field.BooleanField; import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.Field; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.MicronodeField; import com.gentics.mesh.core.rest.node.field.NodeField; import com.gentics.mesh.core.rest.node.field.NumberField; +import com.gentics.mesh.core.rest.node.field.S3BinaryField; import com.gentics.mesh.core.rest.node.field.StringField; import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.FieldList; @@ -34,15 +37,16 @@ import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeOperation; -import com.gentics.mesh.core.rest.schema.impl.*; import com.gentics.mesh.core.rest.schema.impl.BinaryFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NumberFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.S3BinaryFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.StringFieldSchemaImpl; import com.google.common.collect.ImmutableSet; @@ -117,39 +121,42 @@ default R apply(R container) { String newType = getType(); if (newType != null) { - switch (newType) { - case "boolean": + switch (FieldTypes.valueByName(newType)) { + case BOOLEAN: field = new BooleanFieldSchemaImpl(); break; - case "number": + case NUMBER: field = new NumberFieldSchemaImpl(); break; - case "date": + case DATE: field = new DateFieldSchemaImpl(); break; - case "html": + case HTML: field = new HtmlFieldSchemaImpl(); break; - case "string": + case STRING: field = new StringFieldSchemaImpl(); break; - case "binary": + case BINARY: field = new BinaryFieldSchemaImpl(); break; - case "s3binary": + case S3BINARY: field = new S3BinaryFieldSchemaImpl(); break; - case "list": + case LIST: ListFieldSchema listField = new ListFieldSchemaImpl(); listField.setListType(getListType()); field = listField; break; - case "micronode": + case MICRONODE: field = new MicronodeFieldSchemaImpl(); break; - case "node": + case NODE: field = new NodeFieldSchemaImpl(); break; + case JSON: + field = new JsonFieldSchemaImpl(); + break; default: throw error(BAD_REQUEST, "Unknown type {" + newType + "} for change " + getUuid()); } @@ -181,27 +188,29 @@ default R apply(R container) { default Map createFields(FieldSchemaContainer oldSchema, FieldContainer oldContent) { String newType = getType(); - switch (newType) { - case "boolean": + switch (FieldTypes.valueByName(newType)) { + case BOOLEAN: return Collections.singletonMap(getFieldName(), changeToBoolean(oldSchema, oldContent)); - case "number": + case NUMBER: return Collections.singletonMap(getFieldName(), changeToNumber(oldSchema, oldContent)); - case "date": + case DATE: return Collections.singletonMap(getFieldName(), changeToDate(oldSchema, oldContent)); - case "html": + case HTML: return Collections.singletonMap(getFieldName(), changeToHtml(oldSchema, oldContent)); - case "string": + case STRING: return Collections.singletonMap(getFieldName(), changeToString(oldSchema, oldContent)); - case "binary": + case BINARY: return Collections.singletonMap(getFieldName(), changeToBinary(oldSchema, oldContent)); - case "s3binary": + case S3BINARY: return Collections.singletonMap(getFieldName(), changeToS3Binary(oldSchema, oldContent)); - case "list": + case LIST: return Collections.singletonMap(getFieldName(), changeToList(oldSchema, oldContent)); - case "micronode": + case MICRONODE: return Collections.singletonMap(getFieldName(), changeToMicronode(oldSchema, oldContent)); - case "node": + case NODE: return Collections.singletonMap(getFieldName(), changeToNode(oldSchema, oldContent)); + case JSON: + return Collections.singletonMap(getFieldName(), changeToJson(oldSchema, oldContent)); default: throw error(BAD_REQUEST, "Unknown type {" + newType + "} for change " + getUuid()); } @@ -252,6 +261,17 @@ private DateField changeToDate(FieldSchemaContainer oldSchema, FieldContainer ol return new DateFieldImpl().setDate(typeConverter.toDate(oldField.getValue())); } + private JsonField changeToJson(FieldSchemaContainer oldSchema, FieldContainer oldContent) { + String fieldName = getFieldName(); + FieldSchema fieldSchema = oldSchema.getField(fieldName); + + Field oldField = oldContent.getFields().getField(fieldName, fieldSchema); + if (oldField == null) { + return null; + } + return new JsonFieldImpl().setJson(typeConverter.toJsonContent(oldField.getValue())); + } + private HtmlField changeToHtml(FieldSchemaContainer oldSchema, FieldContainer oldContent) { String fieldName = getFieldName(); FieldSchema fieldSchema = oldSchema.getField(fieldName); @@ -318,36 +338,42 @@ private FieldList changeToList(FieldSchemaContainer oldSchema, FieldContainer ol Object oldValue = oldField.getValue(); String oldType = fieldSchema.getType(); - switch (listType) { - case "boolean": + switch (FieldTypes.valueByName(listType)) { + case BOOLEAN: return typeConverter.toBooleanList(oldValue); - case "number": + case NUMBER: if (isUuidType(fieldSchema)) { return null; } - switch (oldType) { - case "number": + switch (FieldTypes.valueByName(oldType)) { + case NUMBER: return new NumberFieldListImpl().setItems(Collections.singletonList(oldContent.getFields().getNumberField(fieldName).getNumber())); default: return typeConverter.toNumberList(oldValue); } - case "date": + case DATE: return typeConverter.toDateList(oldValue); - case "html": + case HTML: if (isNonNodeUuidType(fieldSchema)) { return null; } else { return typeConverter.toHtmlList(oldValue); } - case "string": + case STRING: if (isNonNodeUuidType(fieldSchema)) { return null; } else { return typeConverter.toStringList(oldValue); } - case "micronode": + case JSON: + if (isNonNodeUuidType(fieldSchema)) { + return null; + } else { + return typeConverter.toJsonList(oldValue); + } + case MICRONODE: return typeConverter.toMicronodeList(oldField); - case "node": + case NODE: return typeConverter.toNodeList(oldField); default: throw error(BAD_REQUEST, "Unknown list type {" + listType + "} for change " + getUuid()); @@ -367,6 +393,7 @@ private boolean isUuidType(FieldSchema fieldSchema) { } } + @SuppressWarnings("unused") private FieldList nullableList(T[] input, Supplier> output) { if (input == null) { return null; diff --git a/mdm/common/pom.xml b/mdm/common/pom.xml index 479367fa63..fd1ba1c369 100644 --- a/mdm/common/pom.xml +++ b/mdm/common/pom.xml @@ -28,7 +28,7 @@ com.gentics.mesh mesh-plugin-api - + com.google.dagger diff --git a/mdm/common/src/main/java/com/gentics/mesh/FieldUtil.java b/mdm/common/src/main/java/com/gentics/mesh/FieldUtil.java index 971ec8b2f8..7e7b70633b 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/FieldUtil.java +++ b/mdm/common/src/main/java/com/gentics/mesh/FieldUtil.java @@ -5,8 +5,26 @@ import com.gentics.mesh.core.rest.microschema.impl.MicroschemaCreateRequest; import com.gentics.mesh.core.rest.microschema.impl.MicroschemaModelImpl; import com.gentics.mesh.core.rest.microschema.impl.MicroschemaUpdateRequest; -import com.gentics.mesh.core.rest.node.field.*; -import com.gentics.mesh.core.rest.node.field.impl.*; +import com.gentics.mesh.core.rest.node.field.BinaryField; +import com.gentics.mesh.core.rest.node.field.BooleanField; +import com.gentics.mesh.core.rest.node.field.DateField; +import com.gentics.mesh.core.rest.node.field.Field; +import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.core.rest.node.field.MicronodeField; +import com.gentics.mesh.core.rest.node.field.NumberField; +import com.gentics.mesh.core.rest.node.field.S3BinaryField; +import com.gentics.mesh.core.rest.node.field.StringField; +import com.gentics.mesh.core.rest.node.field.impl.BinaryFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.NodeFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.S3BinaryFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; @@ -15,8 +33,32 @@ import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListItemImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.StringFieldListImpl; -import com.gentics.mesh.core.rest.schema.*; -import com.gentics.mesh.core.rest.schema.impl.*; +import com.gentics.mesh.core.rest.schema.BinaryFieldSchema; +import com.gentics.mesh.core.rest.schema.BooleanFieldSchema; +import com.gentics.mesh.core.rest.schema.DateFieldSchema; +import com.gentics.mesh.core.rest.schema.HtmlFieldSchema; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; +import com.gentics.mesh.core.rest.schema.ListFieldSchema; +import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; +import com.gentics.mesh.core.rest.schema.NodeFieldSchema; +import com.gentics.mesh.core.rest.schema.NumberFieldSchema; +import com.gentics.mesh.core.rest.schema.S3BinaryFieldSchema; +import com.gentics.mesh.core.rest.schema.SchemaVersionModel; +import com.gentics.mesh.core.rest.schema.StringFieldSchema; +import com.gentics.mesh.core.rest.schema.impl.BinaryFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.MicroschemaReferenceImpl; +import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.NumberFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.S3BinaryFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.SchemaCreateRequest; +import com.gentics.mesh.core.rest.schema.impl.SchemaModelImpl; +import com.gentics.mesh.core.rest.schema.impl.StringFieldSchemaImpl; import com.gentics.mesh.util.Tuple; /** @@ -104,6 +146,19 @@ public static StringFieldSchema createStringFieldSchema(String name) { return fieldSchema; } + /** + * Create a new JSON object field schema. + * + * @param name + * Name of the field schema + * @return + */ + public static JsonFieldSchema createJsonFieldSchema(String name) { + JsonFieldSchema fieldSchema = new JsonFieldSchemaImpl(); + fieldSchema.setName(name); + return fieldSchema; + } + /** * Create a string field and set the given value. * @@ -116,12 +171,38 @@ public static StringField createStringField(String stringValue) { return field; } + /** + * Create a JSON object field and set the given value. + * + * @param stringValue + * @return + */ + public static JsonField createJsonField(JsonContent jsonValue) { + JsonField field = new JsonFieldImpl(); + field.setJson(jsonValue); + return field; + } + + /** + * Create a HTML field and set the given value. + * + * @param htmlValue + * @return + */ public static HtmlField createHtmlField(String htmlValue) { HtmlField field = new HtmlFieldImpl(); field.setHTML(htmlValue); return field; } + /** + * Create a binary field and set the given value. + * + * @param uuid target binary UUID + * @param fileName + * @param hashSum + * @return + */ public static BinaryField createBinaryField(String uuid, String fileName, String hashSum) { BinaryField field = new BinaryFieldImpl(); field.setBinaryUuid(uuid); @@ -130,6 +211,12 @@ public static BinaryField createBinaryField(String uuid, String fileName, String return field; } + /** + * Create a S3 binary field and set the given value. + * + * @param fileName + * @return + */ public static S3BinaryField createS3BinaryField(String fileName) { S3BinaryField field = new S3BinaryFieldImpl(); field.setFileName(fileName); @@ -160,6 +247,12 @@ public static BooleanField createBooleanField(Boolean value) { return field; } + /** + * Create a date field and set the given value. + * + * @param iso8601Date + * @return + */ public static DateField createDateField(String iso8601Date) { DateField field = new DateFieldImpl(); field.setDate(iso8601Date); diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestGetters.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestGetters.java index 09145a0599..85f03a22dd 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestGetters.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestGetters.java @@ -22,6 +22,10 @@ public class RestGetters { public static FieldGetter HTML_LIST_GETTER = (container, fieldSchema) -> container.getHTMLList(fieldSchema.getName()); + public static FieldGetter JSON_GETTER = (container, fieldSchema) -> container.getJson(fieldSchema.getName()); + + public static FieldGetter JSON_LIST_GETTER = (container, fieldSchema) -> container.getJsonList(fieldSchema.getName()); + public static FieldGetter MICRONODE_GETTER = (container, fieldSchema) -> container.getMicronode(fieldSchema.getName()); public static FieldGetter MICRONODE_LIST_GETTER = (container, fieldSchema) -> container.getMicronodeList(fieldSchema.getName()); diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestTransformers.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestTransformers.java index 5c8af8ad78..8d05cc61bc 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestTransformers.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestTransformers.java @@ -6,6 +6,7 @@ import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -22,6 +23,8 @@ import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.Field; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.MicronodeField; import com.gentics.mesh.core.rest.node.field.NodeField; import com.gentics.mesh.core.rest.node.field.NumberField; @@ -32,8 +35,10 @@ import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.StringFieldListImpl; +import com.gentics.mesh.json.JsonUtil; import com.gentics.mesh.parameter.LinkType; public interface RestTransformers { @@ -158,6 +163,38 @@ public interface RestTransformers { } }; + FieldTransformer JSON_TRANSFORMER = (container, ac, fieldKey, fieldSchema, languageTags, level, parentNode) -> { + CommonTx tx = CommonTx.get(); + WebRootLinkReplacer webRootLinkReplacer = tx.data().mesh().webRootLinkReplacer(); + HibJsonField sqlJsonField = container.getJson(fieldKey); + if (sqlJsonField == null) { + return null; + } else { + JsonField field = sqlJsonField.transformToRest(ac); + // If needed resolve links within the JSON + if (ac.getNodeParameters().getResolveLinks() != LinkType.OFF) { + HibProject project = tx.getProject(ac); + if (project == null) { + project = parentNode.get().getProject(); + } + field.setJson(JsonContent.fromString(webRootLinkReplacer.replace(ac, tx.getBranch(ac).getUuid(), + ContainerType.forVersion(ac.getVersioningParameters().getVersion()), JsonUtil.toJson(field.getJson(), true), + ac.getNodeParameters().getResolveLinks(), project.getName(), languageTags))); + } + return field; + } + }; + + FieldTransformer JSON_LIST_TRANSFORMER = (container, ac, fieldKey, fieldSchema, languageTags, level, + parentNode) -> { + HibJsonFieldList jsonFieldList = container.getJsonList(fieldKey); + if (jsonFieldList == null) { + return null; + } else { + return jsonFieldList.transformToRest(ac, fieldKey, languageTags, level); + } + }; + FieldTransformer MICRONODE_TRANSFORMER = (container, ac, fieldKey, fieldSchema, languageTags, level, parentNode) -> { HibMicronodeField micronodeGraphField = container.getMicronode(fieldKey); diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestUpdaters.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestUpdaters.java index 5966b5f4f6..984931e08f 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestUpdaters.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/node/field/RestUpdaters.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; import org.apache.commons.collections.CollectionUtils; @@ -26,6 +27,7 @@ import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -37,11 +39,14 @@ import com.gentics.mesh.core.data.s3binary.S3HibBinaryField; import com.gentics.mesh.core.data.schema.HibMicroschemaVersion; import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.rest.JsonSchema; import com.gentics.mesh.core.rest.node.field.BinaryCheckStatus; import com.gentics.mesh.core.rest.node.field.BinaryField; import com.gentics.mesh.core.rest.node.field.BooleanField; import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.MicronodeField; import com.gentics.mesh.core.rest.node.field.NodeField; import com.gentics.mesh.core.rest.node.field.NodeFieldListItem; @@ -57,16 +62,19 @@ import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.StringFieldListImpl; import com.gentics.mesh.core.rest.node.field.s3binary.S3BinaryMetadata; import com.gentics.mesh.core.rest.schema.BinaryFieldSchema; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; import com.gentics.mesh.core.rest.schema.MicroschemaReference; import com.gentics.mesh.core.rest.schema.NodeFieldSchema; import com.gentics.mesh.core.rest.schema.S3BinaryFieldSchema; import com.gentics.mesh.core.rest.schema.StringFieldSchema; +import com.gentics.mesh.json.JsonUtil; import com.gentics.mesh.util.DateUtils; public class RestUpdaters { @@ -366,6 +374,91 @@ public class RestUpdaters { graphBooleanFieldList.createBooleans(booleanList.getItems()); }; + public static FieldUpdater JSON_UPDATER = (container, ac, fieldMap, fieldKey, fieldSchema, schema) -> { + JsonField jsonField = fieldMap.getJsonField(fieldKey); + HibJsonField jsonSqlField = container.getJson(fieldKey); + boolean isJsonFieldSetToNull = fieldMap.hasField(fieldKey) && (jsonField == null || jsonField.getJson() == null); + HibField.failOnDeletionOfRequiredField(jsonSqlField, isJsonFieldSetToNull, fieldSchema, fieldKey, schema); + boolean isJsonFieldNull = jsonField == null || jsonField.getJson() == null; + + // Skip this check for no migrations + if (!ac.isMigrationContext()) { + HibField.failOnMissingRequiredField(jsonSqlField, isJsonFieldNull, fieldSchema, fieldKey, schema); + } + + // Handle Deletion - The field was explicitly set to null and is currently set within the graph thus we must remove it. + if (isJsonFieldSetToNull && jsonSqlField != null) { + container.removeField(jsonSqlField); + return; + } + + // Rest model is empty or null - Abort + if (isJsonFieldNull) { + return; + } + + // check value length + checkStringLength(JsonUtil.toJson(jsonField.getJson()), fieldKey); + + // check value restrictions + JsonFieldSchema jsonFieldSchema = (JsonFieldSchema) fieldSchema; + JsonSchema[] allowedSchemas = jsonFieldSchema.getAllowedSchemas(); + if (allowedSchemas != null && allowedSchemas.length != 0) { + Object jsonContent = jsonField.getJson().getContent(); + if (jsonField.getJson() != null && Arrays.asList(allowedSchemas).stream() + .noneMatch(schema1 -> JsonUtil.newJsonSchemaValidator(schema1.getVertxSchema()).validate(jsonContent).getValid() == Boolean.TRUE)) { + throw error(BAD_REQUEST, "node_error_invalid_json_field_value", fieldKey, JsonUtil.toJson(jsonContent)); + } + } + + // Handle Update / Create - Create new graph field if no existing one could be found + if (jsonSqlField == null) { + container.createJson(fieldKey).setJson(jsonField.getJson()); + } else { + jsonSqlField.setJson(jsonField.getJson()); + } + }; + + public static FieldUpdater JSON_LIST_UPDATER = (container, ac, fieldMap, fieldKey, fieldSchema, schema) -> { + HibJsonFieldList graphJsonFieldList = container.getJsonList(fieldKey); + JsonFieldListImpl jsonList = fieldMap.getJsonFieldList(fieldKey); + boolean isJsonListFieldSetToNull = fieldMap.hasField(fieldKey) && jsonList == null; + HibField.failOnDeletionOfRequiredField(graphJsonFieldList, isJsonListFieldSetToNull, fieldSchema, fieldKey, schema); + boolean restIsNull = jsonList == null; + + // Skip this check for no migrations + if (!ac.isMigrationContext()) { + HibField.failOnMissingRequiredField(graphJsonFieldList, jsonList == null, fieldSchema, fieldKey, schema); + } + + // Handle Deletion + if (isJsonListFieldSetToNull && graphJsonFieldList != null) { + container.removeField(graphJsonFieldList); + return; + } + + // Rest model is empty or null - Abort + if (restIsNull) { + return; + } + + // check strings length + checkStringsLength(jsonList.getItems(), fieldKey, JsonUtil::toJson); + + // Always create a new list. + // This will effectively unlink the old list and create a new one. + // Otherwise the list which is linked to old versions would be updated. + graphJsonFieldList = container.createJsonList(fieldKey); + + // Add items from rest model + for (JsonContent item : jsonList.getItems()) { + if (item == null) { + throw error(BAD_REQUEST, "field_list_error_null_not_allowed", fieldKey); + } + } + graphJsonFieldList.createJsons(jsonList.getItems()); + }; + public static FieldUpdater HTML_UPDATER = (container, ac, fieldMap, fieldKey, fieldSchema, schema) -> { HtmlField htmlField = fieldMap.getHtmlField(fieldKey); HibHtmlField htmlGraphField = container.getHtml(fieldKey); @@ -882,9 +975,18 @@ private static void checkStringLength(String string, String fieldKey) { * @param fieldKey field key */ private static void checkStringsLength(List strings, String fieldKey) { - if (CollectionUtils.isEmpty(strings)) { + checkStringsLength(strings, fieldKey, Function.identity()); + } + + /** + * Check whether all the given strings are within the allowed length bounds. If not, throw an error + * @param list string contents + * @param fieldKey field key + */ + private static void checkStringsLength(List list, String fieldKey, Function toString) { + if (CollectionUtils.isEmpty(list)) { return; } - strings.forEach(string -> checkStringLength(string, fieldKey)); + list.forEach(item -> checkStringLength(toString.apply(item), fieldKey)); } } diff --git a/mdm/common/src/main/java/com/gentics/mesh/core/data/schema/handler/AbstractFieldSchemaContainerComparator.java b/mdm/common/src/main/java/com/gentics/mesh/core/data/schema/handler/AbstractFieldSchemaContainerComparator.java index 3bd8788262..c92982a952 100644 --- a/mdm/common/src/main/java/com/gentics/mesh/core/data/schema/handler/AbstractFieldSchemaContainerComparator.java +++ b/mdm/common/src/main/java/com/gentics/mesh/core/data/schema/handler/AbstractFieldSchemaContainerComparator.java @@ -5,6 +5,7 @@ import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -13,10 +14,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.gentics.mesh.core.rest.JsonSchema; import com.gentics.mesh.core.rest.schema.BinaryExtractOptions; import com.gentics.mesh.core.rest.schema.BinaryFieldSchema; import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.FieldSchemaContainer; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; import com.gentics.mesh.core.rest.schema.MicroschemaModel; @@ -25,6 +28,7 @@ import com.gentics.mesh.core.rest.schema.StringFieldSchema; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel; import com.gentics.mesh.core.rest.schema.impl.BinaryFieldSchemaImpl; +import com.gentics.mesh.json.JsonUtil; import com.gentics.mesh.util.CompareUtils; import io.vertx.core.json.JsonObject; @@ -103,6 +107,10 @@ protected List diff(FC containerA, FC containerB, Class new String[size])); + } if (fieldInB instanceof BinaryFieldSchema) { BinaryFieldSchema field = (BinaryFieldSchema) fieldInB; BinaryExtractOptions options = field.getBinaryExtractOptions(); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/cache/ListableFieldCacheImpl.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/cache/ListableFieldCacheImpl.java index 442dc32e3d..9c4a2d9b9c 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/cache/ListableFieldCacheImpl.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/cache/ListableFieldCacheImpl.java @@ -11,6 +11,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gentics.mesh.cache.impl.EventAwareCacheFactory; import com.gentics.mesh.cache.impl.EventAwareCacheImpl.Builder; @@ -20,19 +22,19 @@ import com.gentics.mesh.core.endpoint.admin.debuginfo.DebugInfoProvider; import com.gentics.mesh.core.endpoint.admin.debuginfo.DebugInfoUtil; import com.gentics.mesh.core.rest.MeshEvent; +import com.gentics.mesh.etc.config.ConfigUtils; import com.gentics.mesh.etc.config.HibernateMeshOptions; import com.gentics.mesh.etc.config.hibernate.HibernateCacheConfig; -import com.gentics.mesh.etc.config.ConfigUtils; import com.gentics.mesh.hibernate.data.domain.AbstractHibListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibDateListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibHtmlListFieldEdgeImpl; +import com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibNumberListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibStringListFieldEdgeImpl; import com.gentics.mesh.hibernate.util.StringScale; +import com.gentics.mesh.json.JsonUtil; import io.reactivex.Flowable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Implementation of {@link ListableFieldCache} @@ -81,7 +83,9 @@ private static int listFieldWeight(UUID uuid, Optional getJavaClass() { switch (fieldType) { + case JSON: + return JsonContent.class; case STRING: case HTML: return String.class; @@ -95,6 +98,13 @@ public Object transformToPersistedValue(Object value) { } else { return ContentColumn.super.transformToPersistedValue(value); } +// if JSON were BLOB, not String +// } else if (fieldType == FieldTypes.JSON) { +// if (value instanceof String) { +// return value.toString().getBytes(StandardCharsets.UTF_8); +// } else { +// return null; +// } } else { return ContentColumn.super.transformToPersistedValue(value); } diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/dagger/HibernateModule.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/dagger/HibernateModule.java index 785dd0e446..5208c379d8 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/dagger/HibernateModule.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/dagger/HibernateModule.java @@ -37,6 +37,7 @@ import com.gentics.mesh.check.DateListItemCheck; import com.gentics.mesh.check.HibernateBranchCheck; import com.gentics.mesh.check.HtmlListItemCheck; +import com.gentics.mesh.check.JsonListItemCheck; import com.gentics.mesh.check.MicronodeFieldRefCheck; import com.gentics.mesh.check.MicronodeListItemCheck; import com.gentics.mesh.check.NodeContentConsistencyCheck; @@ -338,6 +339,7 @@ public static HazelcastInstance hazelcast(HibClusterManager clusterManager) { new NodeFieldContainerCheck(), new NodeFieldContainerVersionsEdgeCheck(), new ContentRefCheck(), + new JsonListItemCheck(), new NodeContentConsistencyCheck()) ); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/database/DefaultSQLDatabase.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/database/DefaultSQLDatabase.java index 51aaaa48f9..9af5cabff4 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/database/DefaultSQLDatabase.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/database/DefaultSQLDatabase.java @@ -10,10 +10,12 @@ import org.hibernate.hikaricp.internal.HikariCPConnectionProvider; import org.hibernate.jpa.boot.spi.IntegratorProvider; import org.hibernate.jpa.boot.spi.JpaSettings; +import org.hibernate.type.format.jackson.JacksonJsonFormatMapper; import com.gentics.mesh.database.connector.DatabaseConnector; import com.gentics.mesh.etc.config.HibernateMeshOptions; import com.gentics.mesh.hibernate.ContentInterceptor; +import com.gentics.mesh.json.JsonUtil; import com.google.common.collect.ImmutableMap; import com.hazelcast.hibernate.HazelcastLocalCacheRegionFactory; @@ -117,6 +119,7 @@ private void setOtherOptions(ImmutableMap.Builder optionBuilder) .put(AvailableSettings.ORDER_UPDATES, "true") .put(AvailableSettings.PHYSICAL_NAMING_STRATEGY, databaseConnector.getPhysicalNamingStrategyClass().getCanonicalName()) .put(AvailableSettings.INTERCEPTOR, ContentInterceptor.class.getName()) + .put(AvailableSettings.JSON_FORMAT_MAPPER, new JacksonJsonFormatMapper(JsonUtil.getMapper())) .put(JpaSettings.INTEGRATOR_PROVIDER, (IntegratorProvider) () -> Collections.singletonList(databaseConnector.getSessionMetadataIntegrator())) // don't save timezones in the dates for backwards compatibility .put(AvailableSettings.PREFERRED_INSTANT_JDBC_TYPE, "TIMESTAMP"); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentDaoImpl.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentDaoImpl.java index 2c098016af..556cc297f8 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentDaoImpl.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentDaoImpl.java @@ -66,6 +66,7 @@ import com.gentics.mesh.core.rest.common.ContainerType; import com.gentics.mesh.core.rest.common.FieldTypes; import com.gentics.mesh.core.rest.common.ReferenceType; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.SchemaModel; @@ -88,6 +89,7 @@ import com.gentics.mesh.hibernate.data.domain.HibFieldEdge; import com.gentics.mesh.hibernate.data.domain.HibHtmlListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibImageVariantImpl; +import com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeContainerImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeListFieldEdgeImpl; @@ -110,6 +112,7 @@ import com.gentics.mesh.hibernate.data.node.field.impl.HibBooleanListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibDateListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibHtmlListFieldImpl; +import com.gentics.mesh.hibernate.data.node.field.impl.HibJsonListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibMicronodeFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibMicronodeListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibNumberListFieldImpl; @@ -525,6 +528,7 @@ public void deleteUnreferencedMicronodes(HibMicroschemaVersion version) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // nothing to do break; @@ -600,6 +604,7 @@ public void delete(Set contentKeys) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // These are stored in the content table, so they will be deleted later on. break; @@ -649,6 +654,7 @@ private void deleteUnreferencedForVersion(HibSchemaVersion version) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // nothing to do break; @@ -870,6 +876,7 @@ public void delete(List containers) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // These are stored in the content table, so they will be deleted later on. break; @@ -953,6 +960,8 @@ private Class getListClass(FieldTypes listType) { return HibStringListFieldEdgeImpl.class; case HTML: return HibHtmlListFieldEdgeImpl.class; + case JSON: + return HibJsonListFieldEdgeImpl.class; case NUMBER: return HibNumberListFieldEdgeImpl.class; case DATE: @@ -1531,6 +1540,7 @@ private void deleteMicronodesList(List containerUuids) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // These are stored in the content table, so they will be deleted later on. break; @@ -1617,6 +1627,7 @@ private void deleteMicronodes(List containerUuids) { case HTML: case NUMBER: case DATE: + case JSON: case BOOLEAN: // These are stored in the content table, so they will be deleted later on. break; @@ -1806,6 +1817,7 @@ public void loadListFields(List containers) { getNumberListFieldValues(getListFieldListUuids(containers, HibNumberListFieldImpl.class)); getHtmlListFieldValues(getListFieldListUuids(containers, HibHtmlListFieldImpl.class)); getStringListFieldValues(getListFieldListUuids(containers, HibStringListFieldImpl.class)); + getJsonListFieldValues(getListFieldListUuids(containers, HibJsonListFieldImpl.class)); getMicronodeListFieldValues(getListFieldListUuids(containers, HibMicronodeListFieldImpl.class)); } @@ -1841,6 +1853,11 @@ public Map> getDateListFieldValues(List listUuids) { return getListValues(listUuids, HibDateListFieldEdgeImpl::getDate, HibDateListFieldEdgeImpl.class); } + @Override + public Map> getJsonListFieldValues(List listUuids) { + return getListValues(listUuids, HibJsonListFieldEdgeImpl::getJson, HibJsonListFieldEdgeImpl.class); + } + @Override public Map> getNumberListFieldValues(List listUuids) { return getListValues(listUuids, HibNumberListFieldEdgeImpl::getNumber, HibNumberListFieldEdgeImpl.class); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentUtils.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentUtils.java index 3eac950b3c..9e8c4ea4e1 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentUtils.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/ContentUtils.java @@ -16,6 +16,7 @@ import com.gentics.mesh.hibernate.data.domain.HibBooleanListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibDateListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibHtmlListFieldEdgeImpl; +import com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibNodeFieldContainerEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibNodeFieldEdgeImpl; @@ -85,6 +86,7 @@ public String[] getHibernateEntityName(Object... args) { case BOOLEAN: return Stream.of(HibBooleanListFieldEdgeImpl.class); case DATE: return Stream.of(HibDateListFieldEdgeImpl.class); case HTML: return Stream.of(HibHtmlListFieldEdgeImpl.class); + case JSON: return Stream.of(HibJsonListFieldEdgeImpl.class); case MICRONODE: return Stream.of(HibMicronodeListFieldEdgeImpl.class, HibMicronodeFieldImpl.class); case NODE: return Stream.of(HibNodeListFieldEdgeImpl.class, HibNodeListFieldImpl.class); default: return Stream.empty(); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/DaoHelper.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/DaoHelper.java index fe47a57f5d..817be756e0 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/DaoHelper.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/dao/DaoHelper.java @@ -100,6 +100,7 @@ import com.gentics.mesh.hibernate.data.domain.HibHtmlListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibImageVariantImpl; import com.gentics.mesh.hibernate.data.domain.HibJobImpl; +import com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl; import com.gentics.mesh.hibernate.data.domain.HibLanguageImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeContainerImpl; import com.gentics.mesh.hibernate.data.domain.HibMicronodeFieldEdgeImpl; @@ -1101,7 +1102,7 @@ private Optional maybeMakeLiteralOperand(FilterOperand opera String paramName = makeParamName(value); if (value != null) { if (UUIDUtil.isUUID(value.toString())) { - if (maybeOperandType.filter(otype -> "string".equalsIgnoreCase(otype) || "html".equalsIgnoreCase(otype)).isEmpty()) { + if (maybeOperandType.filter(otype -> "string".equalsIgnoreCase(otype) || "json".equalsIgnoreCase(otype) || "html".equalsIgnoreCase(otype)).isEmpty()) { value = UUIDUtil.toJavaUuid(value.toString()); } } else if (value.getClass().isEnum()) { @@ -1748,6 +1749,9 @@ private Optional> tableNameIntoClass(String tableName) { case "STRINGLIST": clss = HibStringListFieldEdgeImpl.class; break; + case "JSONLIST": + clss = HibJsonListFieldEdgeImpl.class; + break; case "NUMBERLIST": clss = HibNumberListFieldEdgeImpl.class; break; diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/AbstractHibPropertyContainerElement.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/AbstractHibPropertyContainerElement.java index 385e479fbb..f3627eb296 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/AbstractHibPropertyContainerElement.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/AbstractHibPropertyContainerElement.java @@ -70,6 +70,13 @@ public R property(String name) { for (int i = 0; i < ja.size(); i++) { Object item = ja.getValue(i); item = isJson ? JsonUtil.readValue(item.toString(), cls) : item; + if (!cls.isAssignableFrom(item.getClass())) { + try { + item = cls.getConstructor(item.getClass()).newInstance(item); + } catch (Exception e) { + e.printStackTrace(); + } + } Array.set(array, i, item); } return (R) array; diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibJsonListFieldEdgeImpl.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibJsonListFieldEdgeImpl.java new file mode 100644 index 0000000000..75b1f9154b --- /dev/null +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibJsonListFieldEdgeImpl.java @@ -0,0 +1,61 @@ +package com.gentics.mesh.hibernate.data.domain; + +import java.io.Serializable; +import java.util.UUID; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.database.HibernateTx; + +import jakarta.persistence.Entity; +import jakarta.persistence.Index; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +/** + * JSON object list field definition edge. + * + * @author plyhun + * + */ +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@Entity(name = "jsonlistitem") +@Table(uniqueConstraints = { + @UniqueConstraint( + name = "KeyTypeVersionContainerListIndex", + columnNames = { "itemIndex", "listUuid", "fieldKey", "containerUuid", "containerType", "containerVersionUuid" } + ) +}, indexes = { + @Index(columnList = "listUuid") +}) +public class HibJsonListFieldEdgeImpl extends AbstractHibPrimitiveListFieldEdgeImpl implements HibJsonField, Serializable { + + private static final long serialVersionUID = -6554262711404820079L; + + public HibJsonListFieldEdgeImpl() { + } + + public HibJsonListFieldEdgeImpl(HibernateTx tx, UUID listUuid, int index, String fieldKey, JsonContent value, + HibUnmanagedFieldContainer parentFieldContainer) { + super(tx, listUuid, index, fieldKey, value, parentFieldContainer); + } + + @Override + public JsonContent getJson() { + return valueOrUuid; + } + + @Override + public void setJson(JsonContent value) { + this.valueOrUuid = value; + HibernateTx.get().entityManager().merge(this); + } + + @Override + public boolean equals(Object obj) { + return jsonEquals(obj); + } +} diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibUnmanagedFieldContainer.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibUnmanagedFieldContainer.java index dc2907c491..759e9053a4 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibUnmanagedFieldContainer.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/domain/HibUnmanagedFieldContainer.java @@ -14,6 +14,7 @@ import java.util.function.Supplier; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; import com.gentics.mesh.contentoperation.CommonContentColumn; import com.gentics.mesh.contentoperation.ContentColumn; @@ -29,11 +30,13 @@ import com.gentics.mesh.core.data.node.field.HibBooleanField; import com.gentics.mesh.core.data.node.field.HibDateField; import com.gentics.mesh.core.data.node.field.HibHtmlField; +import com.gentics.mesh.core.data.node.field.HibJsonField; import com.gentics.mesh.core.data.node.field.HibNumberField; import com.gentics.mesh.core.data.node.field.HibStringField; import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -68,6 +71,8 @@ import com.gentics.mesh.hibernate.data.node.field.impl.HibDateListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibHtmlFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibHtmlListFieldImpl; +import com.gentics.mesh.hibernate.data.node.field.impl.HibJsonFieldImpl; +import com.gentics.mesh.hibernate.data.node.field.impl.HibJsonListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibMicronodeFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibMicronodeListFieldImpl; import com.gentics.mesh.hibernate.data.node.field.impl.HibNodeFieldImpl; @@ -79,8 +84,6 @@ import com.gentics.mesh.hibernate.data.node.field.impl.HibStringListFieldImpl; import com.gentics.mesh.util.UUIDUtil; -import org.slf4j.Logger; - /** * A base for the field container entities, that are not known to the Hibernate entity manager. * @@ -239,6 +242,11 @@ default HibStringField getString(String key) { return getFieldValueFromNullableColumn(key, FieldTypes.STRING, HibStringFieldImpl::new); } + @Override + default HibJsonField getJson(String key) { + return getFieldValueFromNullableColumn(key, FieldTypes.JSON, HibJsonFieldImpl::new); + } + @Override default HibBinaryFieldImpl getBinary(String key) { return getReferenceFieldValueFromNullableColumn(key, FieldTypes.BINARY, HibBinaryFieldImpl::new); @@ -328,6 +336,12 @@ default HibBooleanField createBoolean(String key) { return new HibBooleanFieldImpl(key, this, null); } + @Override + default HibJsonField createJson(String key) { + ensureColumnExists(key, FieldTypes.JSON); + return new HibJsonFieldImpl(key, this, null); + } + @Override default HibStringField createString(String key) { ensureColumnExists(key, FieldTypes.STRING); @@ -450,6 +464,11 @@ default HibNumberListFieldImpl getNumberList(String key) { return getListFieldFromNullableColumn(key, FieldTypes.NUMBER, HibNumberListFieldImpl::new); } + @Override + default HibJsonListFieldImpl getJsonList(String key) { + return getListFieldFromNullableColumn(key, FieldTypes.JSON, HibJsonListFieldImpl::new); + } + @Override default HibNumberFieldList createNumberList(String key) { HibernateTx tx = HibernateTx.get(); @@ -458,6 +477,14 @@ default HibNumberFieldList createNumberList(String key) { return HibNumberListFieldImpl.fromContainer(tx, this, key, Collections.emptyList()); } + @Override + default HibJsonFieldList createJsonList(String key) { + HibernateTx tx = HibernateTx.get(); + ensureColumnExists(key, FieldTypes.LIST); + ensureOldReferenceRemoved(tx, key, this::getJsonList, false); + return HibJsonListFieldImpl.fromContainer(tx, this, key, Collections.emptyList()); + } + @Override default HibNodeListFieldImpl getNodeList(String key) { return this.getListFieldFromNullableColumn(key, FieldTypes.NODE, HibNodeListFieldImpl::new); diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/HibFieldTypes.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/HibFieldTypes.java index db3c2731cf..4e0a222991 100644 --- a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/HibFieldTypes.java +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/HibFieldTypes.java @@ -7,6 +7,8 @@ import static com.gentics.mesh.core.data.node.field.RestGetters.DATE_LIST_GETTER; import static com.gentics.mesh.core.data.node.field.RestGetters.HTML_GETTER; import static com.gentics.mesh.core.data.node.field.RestGetters.HTML_LIST_GETTER; +import static com.gentics.mesh.core.data.node.field.RestGetters.JSON_GETTER; +import static com.gentics.mesh.core.data.node.field.RestGetters.JSON_LIST_GETTER; import static com.gentics.mesh.core.data.node.field.RestGetters.MICRONODE_GETTER; import static com.gentics.mesh.core.data.node.field.RestGetters.MICRONODE_LIST_GETTER; import static com.gentics.mesh.core.data.node.field.RestGetters.NODE_GETTER; @@ -23,6 +25,8 @@ import static com.gentics.mesh.core.data.node.field.RestTransformers.DATE_TRANSFORMER; import static com.gentics.mesh.core.data.node.field.RestTransformers.HTML_LIST_TRANSFORMER; import static com.gentics.mesh.core.data.node.field.RestTransformers.HTML_TRANSFORMER; +import static com.gentics.mesh.core.data.node.field.RestTransformers.JSON_LIST_TRANSFORMER; +import static com.gentics.mesh.core.data.node.field.RestTransformers.JSON_TRANSFORMER; import static com.gentics.mesh.core.data.node.field.RestTransformers.MICRONODE_LIST_TRANSFORMER; import static com.gentics.mesh.core.data.node.field.RestTransformers.MICRONODE_TRANSFORMER; import static com.gentics.mesh.core.data.node.field.RestTransformers.NODE_LIST_TRANSFORMER; @@ -39,6 +43,8 @@ import static com.gentics.mesh.core.data.node.field.RestUpdaters.DATE_UPDATER; import static com.gentics.mesh.core.data.node.field.RestUpdaters.HTML_LIST_UPDATER; import static com.gentics.mesh.core.data.node.field.RestUpdaters.HTML_UPDATER; +import static com.gentics.mesh.core.data.node.field.RestUpdaters.JSON_LIST_UPDATER; +import static com.gentics.mesh.core.data.node.field.RestUpdaters.JSON_UPDATER; import static com.gentics.mesh.core.data.node.field.RestUpdaters.MICRONODE_LIST_UPDATER; import static com.gentics.mesh.core.data.node.field.RestUpdaters.MICRONODE_UPDATER; import static com.gentics.mesh.core.data.node.field.RestUpdaters.NODE_LIST_UPDATER; @@ -83,6 +89,8 @@ public enum HibFieldTypes { BOOLEAN_LIST("list.boolean", BOOLEAN_LIST_TRANSFORMER, BOOLEAN_LIST_UPDATER, BOOLEAN_LIST_GETTER), HTML("html", HTML_TRANSFORMER, HTML_UPDATER, HTML_GETTER), HTML_LIST("list.html", HTML_LIST_TRANSFORMER, HTML_LIST_UPDATER, HTML_LIST_GETTER), + JSON("json", JSON_TRANSFORMER, JSON_UPDATER, JSON_GETTER), + JSON_LIST("list.json", JSON_LIST_TRANSFORMER, JSON_LIST_UPDATER, JSON_LIST_GETTER), MICRONODE("micronode", MICRONODE_TRANSFORMER, MICRONODE_UPDATER, MICRONODE_GETTER), MICRONODE_LIST("list.micronode", MICRONODE_LIST_TRANSFORMER, MICRONODE_LIST_UPDATER, MICRONODE_LIST_GETTER), NODE("node", NODE_TRANSFORMER, NODE_UPDATER, NODE_GETTER), diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonFieldImpl.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonFieldImpl.java new file mode 100644 index 0000000000..8365ce2e7f --- /dev/null +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonFieldImpl.java @@ -0,0 +1,34 @@ +package com.gentics.mesh.hibernate.data.node.field.impl; + +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.hibernate.data.domain.HibUnmanagedFieldContainer; + +/** + * JSON object field of Hibernate content. + * + * @author plyhun + * + */ +public class HibJsonFieldImpl extends AbstractBasicHibField implements HibJsonField { + + public HibJsonFieldImpl(String fieldKey, HibUnmanagedFieldContainer parent, JsonContent value) { + super(fieldKey, parent, FieldTypes.JSON, value); + } + + @Override + public JsonContent getJson() { + return valueOrNull(); + } + + @Override + public void setJson(JsonContent string) { + storeValue(string); + } + + @Override + public boolean equals(Object obj) { + return jsonEquals(obj); + } +} diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonListFieldImpl.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonListFieldImpl.java new file mode 100644 index 0000000000..f2c36984f9 --- /dev/null +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/data/node/field/impl/HibJsonListFieldImpl.java @@ -0,0 +1,88 @@ +package com.gentics.mesh.hibernate.data.node.field.impl; + +import java.util.List; +import java.util.UUID; +import java.util.stream.IntStream; + +import com.gentics.mesh.core.data.HibField; +import com.gentics.mesh.core.data.HibFieldContainer; +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; +import com.gentics.mesh.database.HibernateTx; +import com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl; +import com.gentics.mesh.hibernate.data.domain.HibUnmanagedFieldContainer; + +/** + * JSON object list field implementation. + * + * @author plyhun + * + */ +public class HibJsonListFieldImpl extends + AbstractHibHeterogenicPrimitiveListFieldImpl + implements HibJsonFieldList { + + protected HibJsonListFieldImpl(HibernateTx tx, String fieldKey, HibUnmanagedFieldContainer parent) { + super(tx, fieldKey, parent, HibJsonListFieldEdgeImpl.class); + } + + public HibJsonListFieldImpl(String fieldKey, HibUnmanagedFieldContainer parent, UUID initialValue) { + super(initialValue, fieldKey, parent, HibJsonListFieldEdgeImpl.class); + } + + @Override + public HibField cloneTo(HibFieldContainer container) { + HibernateTx tx = HibernateTx.get(); + HibUnmanagedFieldContainer unmanagedBase = (HibUnmanagedFieldContainer) container; + unmanagedBase.ensureColumnExists(getFieldKey(), FieldTypes.LIST); + unmanagedBase.ensureOldReferenceRemoved(tx, getFieldKey(), unmanagedBase::getJsonList, false); + return HibJsonListFieldImpl.fromContainer(tx, unmanagedBase, getFieldKey(), getValues()); + } + + @Override + public HibJsonField createJson(JsonContent value) { + return createItem(value); + } + + @Override + public void createJsons(List items) { + createItems(items); + } + + @Override + public HibJsonField getJson(int index) { + return get(index); + } + + /** + * Make an edge for the given container, field key and values. + * + * @param tx + * @param container + * @param fieldKey + * @param values + * @return + */ + public static HibJsonListFieldImpl fromContainer(HibernateTx tx, + HibUnmanagedFieldContainer container, String fieldKey, List values) { + HibJsonListFieldImpl list = new HibJsonListFieldImpl(tx, fieldKey, container); + IntStream.range(0, values.size()).mapToObj( + i -> new HibJsonListFieldEdgeImpl(tx, list.valueOrNull(), i, fieldKey, values.get(i), container)) + .forEach(item -> tx.entityManager().persist(item)); + return list; + } + + @Override + protected JsonContent getValue(HibJsonField field) { + return field.getJson(); + } + + @Override + protected HibListFieldItemConstructor getItemConstructor() { + return HibJsonListFieldEdgeImpl::new; + } + +} diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/CustomTypeContributor.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/CustomTypeContributor.java new file mode 100644 index 0000000000..44301ca97a --- /dev/null +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/CustomTypeContributor.java @@ -0,0 +1,16 @@ +package com.gentics.mesh.hibernate.type; + +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.service.ServiceRegistry; + +/** + * Custom Mesh SQL/Java type contributor. + */ +public class CustomTypeContributor implements TypeContributor { + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + typeContributions.contributeType(JsonContentType.INSTANCE); + } +} diff --git a/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/JsonContentType.java b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/JsonContentType.java new file mode 100644 index 0000000000..d7122a6109 --- /dev/null +++ b/mdm/hibernate-core/src/main/java/com/gentics/mesh/hibernate/type/JsonContentType.java @@ -0,0 +1,80 @@ +package com.gentics.mesh.hibernate.type; + +import java.io.Serializable; +import java.io.StringWriter; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.SqlTypes; +import org.hibernate.usertype.UserType; + +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.json.JsonUtil; + +/** + * JSON content mapping type + */ +public class JsonContentType implements UserType { + + public static final JsonContentType INSTANCE = new JsonContentType(); + + @Override + public JsonContent nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) throws SQLException { + final String cellContent = rs.getString(position); + if (cellContent == null) { + return null; + } + try { + return JsonUtil.getMapper().readValue(cellContent.getBytes("UTF-8"), returnedClass()); + } catch (final Exception ex) { + throw new RuntimeException("Failed to convert String to JsonObject: " + ex.getMessage(), ex); + } + } + + @Override + public Serializable disassemble(JsonContent value) { + if (value == null) { + return null; + } + return JsonUtil.toJson(value); + } + + @Override + public void nullSafeSet(PreparedStatement st, JsonContent value, int index, SharedSessionContractImplementor session) throws SQLException { + if (value == null) { + st.setNull(index, SqlTypes.LONGVARCHAR); + return; + } + try { + final StringWriter w = new StringWriter(); + JsonUtil.getMapper().writeValue(w, value); + w.flush(); + st.setObject(index, w.toString(), SqlTypes.LONGVARCHAR); + } catch (final Exception ex) { + throw new RuntimeException("Failed to convert JsonObject to String: " + ex.getMessage(), ex); + } + } + + @Override + public JsonContent deepCopy(JsonContent value) { + String value1 = JsonUtil.toJson(value); + return JsonUtil.readValue(value1, JsonContent.class); + } + + @Override + public int getSqlType() { + return SqlTypes.JSON; + } + + @Override + public Class returnedClass() { + return JsonContent.class; + } + + @Override + public boolean isMutable() { + return true; + } +} diff --git a/mdm/hibernate-core/src/main/resources/META-INF/persistence.xml b/mdm/hibernate-core/src/main/resources/META-INF/persistence.xml index 36b0aee30f..b00620ac2d 100644 --- a/mdm/hibernate-core/src/main/resources/META-INF/persistence.xml +++ b/mdm/hibernate-core/src/main/resources/META-INF/persistence.xml @@ -18,6 +18,7 @@ com.gentics.mesh.hibernate.data.domain.HibDateListFieldEdgeImpl com.gentics.mesh.hibernate.data.domain.HibFieldTypeChangeImpl com.gentics.mesh.hibernate.data.domain.HibGroupImpl + com.gentics.mesh.hibernate.data.domain.HibJsonListFieldEdgeImpl com.gentics.mesh.hibernate.data.domain.HibHtmlListFieldEdgeImpl com.gentics.mesh.hibernate.data.domain.HibImageVariantImpl com.gentics.mesh.hibernate.data.domain.HibJobImpl diff --git a/mdm/hibernate-core/src/main/resources/META-INF/services/org.hibernate.boot.model.TypeContributor b/mdm/hibernate-core/src/main/resources/META-INF/services/org.hibernate.boot.model.TypeContributor new file mode 100644 index 0000000000..da329c7082 --- /dev/null +++ b/mdm/hibernate-core/src/main/resources/META-INF/services/org.hibernate.boot.model.TypeContributor @@ -0,0 +1 @@ +com.gentics.mesh.hibernate.type.CustomTypeContributor \ No newline at end of file diff --git a/rest-model/pom.xml b/rest-model/pom.xml index f60789fc2f..fe6f352d89 100644 --- a/rest-model/pom.xml +++ b/rest-model/pom.xml @@ -56,6 +56,10 @@ commons-io commons-io + + io.vertx + vertx-json-schema + org.raml raml-parser diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchema.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchema.java new file mode 100644 index 0000000000..b6ad4f4502 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchema.java @@ -0,0 +1,81 @@ +package com.gentics.mesh.core.rest; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.tuple.Pair; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.gentics.mesh.core.rest.common.RestModel; +import com.gentics.mesh.json.JsonUtil; + +import io.vertx.core.json.JsonObject; + +/** + * A wrapper of serializable {@link JsonObject} to produce {@link io.vertx.reactivex.json.schema.JsonSchema}s out of it. + */ +public class JsonSchema extends JsonSchemaType { + + private String[] required = new String[0]; + private Map properties = new HashMap<>(); + + + public JsonSchema() { + super(); + } + + public JsonSchema(JsonObject object) { + super((object == null || object.getString("type") == null) ? "object" : object.getString("type")); + setRequired((object == null || object.getJsonArray("required") == null) + ? new String[0] + : object.getJsonArray("required").stream().map(Object::toString).toArray(size -> new String[size])); + setProperties((object == null || object.getJsonObject("properties") == null) + ? new HashMap<>() + : object.getJsonObject("properties").getMap().entrySet().stream() + .map(e -> Pair.of(e.getKey(), JsonUtil.readValue(JsonUtil.toJson(e.getValue()), JsonSchemaType.class))) + .collect(Collectors.toMap(Pair::getKey, Pair::getValue))); + } + public JsonSchema(String json) { + this(json == null ? null : new JsonObject(json)); + } + + @Override + public JsonSchema setType(String type) { + super.setType(type); + return this; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof RestModel) ? JsonUtil.equals(this, (RestModel) obj) : false; + } + + @Override + public int hashCode() { + return JsonUtil.toJson(this).hashCode(); + } + + @JsonIgnore + public io.vertx.reactivex.json.schema.JsonSchema getVertxSchema() { + return io.vertx.reactivex.json.schema.JsonSchema.of(new JsonObject(toJson())); + } + + public String[] getRequired() { + return required; + } + + public JsonSchema setRequired(String[] required) { + this.required = required; + return this; + } + + public Map getProperties() { + return properties; + } + + public JsonSchema setProperties(Map properties) { + this.properties = properties; + return this; + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchemaType.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchemaType.java new file mode 100644 index 0000000000..980cc4755d --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/JsonSchemaType.java @@ -0,0 +1,27 @@ +package com.gentics.mesh.core.rest; + +import com.gentics.mesh.core.rest.common.RestModel; + +/** + * Serializable JSON type container. + */ +public class JsonSchemaType implements RestModel { + + private String type = "object"; + + public JsonSchemaType() { + } + + public JsonSchemaType(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public JsonSchemaType setType(String type) { + this.type = type; + return this; + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/common/FieldTypes.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/common/FieldTypes.java index 38977a2337..06f4899a19 100644 --- a/rest-model/src/main/java/com/gentics/mesh/core/rest/common/FieldTypes.java +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/common/FieldTypes.java @@ -16,6 +16,8 @@ public enum FieldTypes { HTML(HtmlFieldSchema.class, HtmlFieldSchemaImpl.class, HtmlField.class, HtmlFieldImpl.class), + JSON(JsonFieldSchema.class, JsonFieldSchemaImpl.class, JsonField.class, JsonFieldImpl.class), + NUMBER(NumberFieldSchema.class, NumberFieldSchemaImpl.class, NumberField.class, NumberFieldImpl.class), DATE(DateFieldSchema.class, DateFieldSchemaImpl.class, DateField.class, DateFieldImpl.class), diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMap.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMap.java index 77905d5395..c718072e7b 100644 --- a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMap.java +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMap.java @@ -15,6 +15,7 @@ import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.MicronodeFieldList; @@ -22,6 +23,7 @@ import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.StringFieldListImpl; import com.gentics.mesh.core.rest.schema.FieldSchema; @@ -106,6 +108,22 @@ default Field putString(String fieldKey, String string) { */ NumberFieldListImpl getNumberFieldList(String fieldKey); + /** + * Return the JSON object field with the given key + * + * @param fieldKey + * @return + */ + JsonFieldImpl getJsonField(String fieldKey); + + /** + * Return the JSON object list field with the given key. + * + * @param fieldKey + * @return + */ + JsonFieldListImpl getJsonFieldList(String fieldKey); + /** * Return the html field with the given key * diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMapImpl.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMapImpl.java index f10ef7899c..e865b53611 100644 --- a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMapImpl.java +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/FieldMapImpl.java @@ -21,14 +21,34 @@ import com.fasterxml.jackson.databind.node.POJONode; import com.gentics.mesh.core.rest.common.FieldTypes; import com.gentics.mesh.core.rest.micronode.MicronodeResponse; -import com.gentics.mesh.core.rest.node.field.*; -import com.gentics.mesh.core.rest.node.field.impl.*; +import com.gentics.mesh.core.rest.node.field.BinaryField; +import com.gentics.mesh.core.rest.node.field.BooleanField; +import com.gentics.mesh.core.rest.node.field.DateField; +import com.gentics.mesh.core.rest.node.field.Field; +import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.core.rest.node.field.NodeField; +import com.gentics.mesh.core.rest.node.field.NodeFieldListItem; +import com.gentics.mesh.core.rest.node.field.NumberField; +import com.gentics.mesh.core.rest.node.field.S3BinaryField; +import com.gentics.mesh.core.rest.node.field.StringField; +import com.gentics.mesh.core.rest.node.field.impl.BinaryFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.NodeFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.S3BinaryFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.FieldList; import com.gentics.mesh.core.rest.node.field.list.MicronodeFieldList; import com.gentics.mesh.core.rest.node.field.list.NodeFieldList; import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.MicronodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; @@ -39,6 +59,9 @@ import com.gentics.mesh.json.JsonUtil; import com.google.common.collect.Lists; +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + /** * Implementation of a fieldmap which uses a central JsonNode to access the field specific data. Fields will be mapped during runtime. * @@ -83,6 +106,8 @@ public T getField(String key, FieldTypes type, String listType return (T) transformNumberFieldJsonNode(jsonNode, key); case BOOLEAN: return (T) transformBooleanFieldJsonNode(jsonNode, key); + case JSON: + return (T) transformJsonFieldJsonNode(jsonNode, key); case DATE: return (T) transformDateFieldJsonNode(jsonNode, key); case LIST: @@ -104,8 +129,8 @@ private FieldList transformListFieldJsonNode(JsonNode jsonNode, String key, S ObjectMapper mapper = JsonUtil.getMapper(); // ListFieldSchemaImpl listFieldSchema = (ListFieldSchemaImpl) fieldSchema; - switch (listType) { - case "node": + switch (FieldTypes.valueByName(listType)) { + case NODE: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, NodeFieldList.class, key); @@ -133,7 +158,7 @@ private FieldList transformListFieldJsonNode(JsonNode jsonNode, String key, S // throw new MeshJsonException("Could not read node field for key {" + fieldKey + "}", e); // } return nodeListField; - case "micronode": + case MICRONODE: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, MicronodeFieldList.class, key); @@ -144,35 +169,42 @@ private FieldList transformListFieldJsonNode(JsonNode jsonNode, String key, S } return micronodeFieldList; // Basic types - case "string": + case STRING: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, StringFieldListImpl.class, key); } String[] itemsStringArray = mapper.treeToValue(jsonNode, String[].class); return getBasicList(key, String[].class, new StringFieldListImpl(), String.class, itemsStringArray); - case "html": + case HTML: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, HtmlFieldListImpl.class, key); } String[] itemsHtmlArray = mapper.treeToValue(jsonNode, String[].class); return getBasicList(key, String[].class, new HtmlFieldListImpl(), String.class, itemsHtmlArray); - case "date": + case DATE: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, DateFieldListImpl.class, key); } String[] itemsDateArray = mapper.treeToValue(jsonNode, String[].class); return getBasicList(key, String[].class, new DateFieldListImpl(), String.class, itemsDateArray); - case "number": + case JSON: + // Unwrap stored pojos + if (jsonNode.isPojo()) { + return pojoNodeToValue(jsonNode, JsonFieldListImpl.class, key); + } + JsonContent[] itemsJsonArray = mapper.treeToValue(jsonNode, JsonContent[].class); + return getBasicList(key, JsonObject[].class, new JsonFieldListImpl(), JsonContent.class, itemsJsonArray); + case NUMBER: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, NumberFieldListImpl.class, key); } Number[] itemsNumberArray = mapper.treeToValue(jsonNode, Number[].class); return getBasicList(key, Number[].class, new NumberFieldListImpl(), Number.class, itemsNumberArray); - case "boolean": + case BOOLEAN: // Unwrap stored pojos if (jsonNode.isPojo()) { return pojoNodeToValue(jsonNode, BooleanFieldListImpl.class, key); @@ -331,6 +363,30 @@ private BooleanField transformBooleanFieldJsonNode(JsonNode jsonNode, String key return booleanField; } + private JsonField transformJsonFieldJsonNode(JsonNode jsonNode, String key) { + // Unwrap stored pojos + if (jsonNode.isPojo()) { + JsonField field = pojoNodeToValue(jsonNode, JsonField.class, key); + if (field == null || field.getValue() == null) { + return null; + } else { + return field; + } + } + + JsonField jsonField = new JsonFieldImpl(); + if (!jsonNode.isNull()) { + if (jsonNode.isArray()) { + jsonField.setJson(JsonContent.fromArray(new JsonArray(jsonNode.toString()))); + } else if (jsonNode.isObject()) { + jsonField.setJson(JsonContent.fromObject(new JsonObject(jsonNode.toString()))); + } else { + throw error(BAD_REQUEST, "The field value for {" + key + "} is not a valid JSON. The value was {" + jsonNode.get("json") + "}"); + } + } + return jsonField; + } + private NumberField transformNumberFieldJsonNode(JsonNode jsonNode, String key) { // Unwrap stored pojos if (jsonNode.isPojo()) { @@ -445,6 +501,11 @@ public HtmlFieldImpl getHtmlField(String key) { return getField(key, FieldTypes.HTML); } + @Override + public JsonFieldImpl getJsonField(String key) { + return getField(key, FieldTypes.JSON); + } + @Override public BinaryField getBinaryField(String key) { return getField(key, FieldTypes.BINARY); @@ -514,6 +575,11 @@ public NumberFieldListImpl getNumberFieldList(String key) { return getField(key, FieldTypes.LIST, "number"); } + @Override + public JsonFieldListImpl getJsonFieldList(String key) { + return getField(key, FieldTypes.LIST, "json"); + } + @Override public BooleanFieldListImpl getBooleanFieldList(String key) { return getField(key, FieldTypes.LIST, "boolean"); diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonContent.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonContent.java new file mode 100644 index 0000000000..89af6c4e28 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonContent.java @@ -0,0 +1,153 @@ +package com.gentics.mesh.core.rest.node.field; + +import java.util.Objects; + +import org.apache.commons.lang.StringUtils; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.gentics.mesh.json.JsonUtil; + +import io.vertx.core.json.JsonArray; +import io.vertx.core.json.JsonObject; + +/** + * JSON POJO, common for JSON objects and arrays. + */ +public class JsonContent { + + @JsonIgnore + private final String jsonString; + + /** + * Inner ctor, accepting an already validated json content (array/object) string. + * + * @param jsonString + */ + protected JsonContent(String jsonString) { + this.jsonString = jsonString; + } + + /** + * Is this content a JSON array? + * + * @return + */ + @JsonIgnore + public boolean isArray() { + return StringUtils.isNotBlank(jsonString) && jsonString.startsWith("[") && jsonString.endsWith("]"); + } + + /** + * Get either a {@link JsonObject} or {@link JsonArray} or null out of this content. + * + * @return + */ + @JsonIgnore + public Object getContent() { + if (isArray()) { + return new JsonArray(jsonString); + } else if (StringUtils.isNotBlank(jsonString)) { + return new JsonObject(jsonString); + } else { + return null; + } + } + + /** + * Get a {@link JsonObject} of this content, if exists and applicable. + * + * @return + */ + @JsonIgnore + public JsonObject getObject() { + if (isArray() || StringUtils.isBlank(jsonString)) { + return null; + } else { + return new JsonObject(jsonString); + } + } + + /** + * Get a {@link JsonArray} of this content, if exists and applicable. + * + * @return + */ + @JsonIgnore + public JsonArray getArray() { + if (isArray()) { + return new JsonArray(jsonString); + } else { + return null; + } + } + + /** + * Utility method for creating a JsonObject from the JSON object. + * + * @param object + * @return + */ + public static JsonContent fromObject(JsonObject object) { + return new JsonContent(JsonUtil.toJson(object)); + } + + /** + * Utility method for creating a JsonObject from the JSON array. + * + * @param array + * @return + */ + public static JsonContent fromArray(JsonArray array) { + return new JsonContent(JsonUtil.toJson(array)); + } + + /** + * Get a string representation of the JSON content. + * + * @return + */ + @JsonIgnore + public String getString() { + return jsonString; + } + + /** + * Utility method for creating a JsonObject from the JSON object/array string representation. May return null. + * + * @param jsonString + * @return + */ + public static JsonContent fromString(String jsonString) { + if (StringUtils.isNotBlank(jsonString)) { + if (jsonString.trim().startsWith("[")) { + return new JsonContent(JsonUtil.toJson(JsonUtil.readValue(jsonString, JsonArray.class), true)); + } else { + return new JsonContent(JsonUtil.toJson(JsonUtil.readValue(jsonString, JsonObject.class), true)); + } + } + return null; + } + + @Override + @JsonIgnore + public boolean equals(Object obj) { + if (obj instanceof JsonContent jc) { + boolean meArray = isArray(); + boolean itArray = jc.isArray(); + if (meArray ^ itArray) { + return false; + } else if (meArray) { + return Objects.equals(getArray(), jc.getArray()); + } else { + return Objects.equals(getObject(), jc.getObject()); + } + } + return false; + } + + @Override + @JsonIgnore + public int hashCode() { + return isArray() ? getArray().hashCode() : Objects.hashCode(getObject()); + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonField.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonField.java new file mode 100644 index 0000000000..bd9dcedf06 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/JsonField.java @@ -0,0 +1,39 @@ +package com.gentics.mesh.core.rest.node.field; + +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; + +/** + * REST POJO for the JSON formatted information. + */ +public interface JsonField extends ListableField, MicroschemaListableField { + + /** + * Get the stored JSON object + * + * @return + */ + JsonContent getJson(); + + /** + * Store the given JSON object + * + * @param json + * @return + */ + JsonField setJson(JsonContent json); + + @Override + default Object getValue() { + return getJson(); + } + + /** + * Shortcut for the fast REST model field creation. + * + * @param json + * @return + */ + static JsonField of(JsonContent json) { + return new JsonFieldImpl().setJson(json); + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/impl/JsonFieldImpl.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/impl/JsonFieldImpl.java new file mode 100644 index 0000000000..fcc9cb7787 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/impl/JsonFieldImpl.java @@ -0,0 +1,37 @@ +package com.gentics.mesh.core.rest.node.field.impl; + +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.json.JsonUtil; + +/** + * @see JsonField + */ +public class JsonFieldImpl implements JsonField { + + @JsonPropertyDescription("JSON field value") + private JsonContent json; + + @Override + public String getType() { + return FieldTypes.JSON.toString(); + } + + @Override + public JsonContent getJson() { + return json; + } + + @Override + public JsonField setJson(JsonContent json) { + this.json = json; + return this; + } + + @Override + public String toString() { + return JsonUtil.toJson(getJson()); + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/list/impl/JsonFieldListImpl.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/list/impl/JsonFieldListImpl.java new file mode 100644 index 0000000000..115b4bf53d --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/node/field/list/impl/JsonFieldListImpl.java @@ -0,0 +1,16 @@ +package com.gentics.mesh.core.rest.node.field.list.impl; + +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.FieldMap; +import com.gentics.mesh.core.rest.node.field.JsonContent; + +/** + * REST model for a JSON object list field. Please note that {@link FieldMap} will handle the actual JSON format building. + */ +public class JsonFieldListImpl extends AbstractFieldList { + + @Override + public String getItemType() { + return FieldTypes.JSON.toString(); + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/JsonFieldSchema.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/JsonFieldSchema.java new file mode 100644 index 0000000000..a4a193b082 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/JsonFieldSchema.java @@ -0,0 +1,25 @@ +package com.gentics.mesh.core.rest.schema; + +import com.gentics.mesh.core.rest.JsonSchema; + +/** + * REST POJO for a JSON object field schema. + */ +public interface JsonFieldSchema extends FieldSchema { + + /** + * Return a list of values which are allowed for this field. Null if no value restriction set + * + * @return Allowed values + */ + JsonSchema[] getAllowedSchemas(); + + /** + * Set the list of values which are allowed for this field. Set to null to remove value restriction + * + * @param allowedSchemas + * Allowed values or null + * @return Fluent API + */ + JsonFieldSchema setAllowedSchemas(JsonSchema... allowedSchemas); +} diff --git a/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/impl/JsonFieldSchemaImpl.java b/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/impl/JsonFieldSchemaImpl.java new file mode 100644 index 0000000000..db90472e7a --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/core/rest/schema/impl/JsonFieldSchemaImpl.java @@ -0,0 +1,68 @@ +package com.gentics.mesh.core.rest.schema.impl; + +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel.ALLOW_KEY; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.gentics.mesh.core.rest.JsonSchema; +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; + +import io.vertx.core.json.JsonArray; + +/** + * @see JsonFieldSchema + */ +public class JsonFieldSchemaImpl extends AbstractFieldSchema implements JsonFieldSchema { + + @JsonProperty("allow") + private JsonSchema[] allowedSchemas; + + @Override + public String getType() { + return FieldTypes.JSON.toString(); + } + + @Override + public JsonSchema[] getAllowedSchemas() { + return allowedSchemas; + } + + @Override + public JsonFieldSchemaImpl setAllowedSchemas(JsonSchema... allowedSchemas) { + this.allowedSchemas = allowedSchemas; + return this; + } + + @Override + public Map getAllChangeProperties() { + Map map = super.getAllChangeProperties(); + map.put(ALLOW_KEY, getAllowedSchemas()); + return map; + } + + @Override + public void apply(Map fieldProperties) { + super.apply(fieldProperties); + Object allowedValues = fieldProperties.get(ALLOW_KEY); + if (allowedValues != null) { + if (allowedValues instanceof JsonSchema[] allowedSchemas) { + setAllowedSchemas(allowedSchemas); + } else if (allowedValues instanceof String[]) { + String[] values = (String[]) allowedValues; + setAllowedSchemas(Arrays.stream(values).map(JsonSchema::new).toArray(size -> new JsonSchema[size])); + } else if (allowedValues instanceof Collection) { + setAllowedSchemas(((Collection) allowedValues).stream().map(Object::toString).map(JsonSchema::new).toArray(size -> new JsonSchema[size])); + } else if (allowedValues instanceof Object[]) { + setAllowedSchemas(Arrays.stream(((Object[]) allowedValues)).map(Object::toString).map(JsonSchema::new).toArray(size -> new JsonSchema[size])); + } else if (allowedValues instanceof JsonArray) { + setAllowedSchemas(((JsonArray) allowedValues).stream().map(Object::toString).map(JsonSchema::new).toArray(size -> new JsonSchema[size])); + } else { + throw new IllegalStateException("Unsupported allowed value type: " + allowedValues.getClass().getCanonicalName()); + } + } + } +} diff --git a/rest-model/src/main/java/com/gentics/mesh/json/JsonUtil.java b/rest-model/src/main/java/com/gentics/mesh/json/JsonUtil.java index 9b495f6ab5..b3081c096c 100644 --- a/rest-model/src/main/java/com/gentics/mesh/json/JsonUtil.java +++ b/rest-model/src/main/java/com/gentics/mesh/json/JsonUtil.java @@ -4,8 +4,11 @@ import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import java.io.IOException; +import java.util.Comparator; import org.codehaus.jettison.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -25,6 +28,7 @@ import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import com.gentics.mesh.core.rest.common.RestModel; import com.gentics.mesh.core.rest.error.AbstractRestException; import com.gentics.mesh.core.rest.error.GenericRestException; import com.gentics.mesh.core.rest.event.EventCauseInfo; @@ -32,11 +36,13 @@ import com.gentics.mesh.core.rest.microschema.impl.MicroschemaModelImpl; import com.gentics.mesh.core.rest.node.FieldMap; import com.gentics.mesh.core.rest.node.FieldMapImpl; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.ListableField; import com.gentics.mesh.core.rest.node.field.NodeFieldListItem; import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.FieldList; @@ -54,6 +60,7 @@ import com.gentics.mesh.json.deserializer.FieldMapDeserializer; import com.gentics.mesh.json.deserializer.FieldSchemaDeserializer; import com.gentics.mesh.json.deserializer.JsonArrayDeserializer; +import com.gentics.mesh.json.deserializer.JsonContentDeserializer; import com.gentics.mesh.json.deserializer.JsonObjectDeserializer; import com.gentics.mesh.json.deserializer.NodeFieldListItemDeserializer; import com.gentics.mesh.json.deserializer.PermissionChangedEventModelDeserializer; @@ -62,18 +69,41 @@ import com.gentics.mesh.json.serializer.BasicFieldSerializer; import com.gentics.mesh.json.serializer.FieldListSerializer; import com.gentics.mesh.json.serializer.JsonArraySerializer; +import com.gentics.mesh.json.serializer.JsonContentSerializer; import com.gentics.mesh.json.serializer.JsonObjectSerializer; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import io.vertx.json.schema.Draft; +import io.vertx.json.schema.JsonSchemaOptions; +import io.vertx.reactivex.json.schema.JsonSchema; +import io.vertx.reactivex.json.schema.Validator; /** * Main JSON Util which is used to register all custom JSON specific handlers and deserializers. */ public final class JsonUtil { + /** + * JSON object comparator + */ + public static Comparator COMPARATOR = (a,b) -> { + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + if (a.equals(b)) { + return 0; + } else { + return a.toString().compareTo(b.toString()); + } + }; + protected static ObjectMapper defaultMapper; protected static JsonSchemaGenerator schemaGen; protected static PrettyPrinter minifyingPrettyPrinter; @@ -104,9 +134,11 @@ private static void initDefaultMapper() { module.addSerializer(StringFieldImpl.class, new BasicFieldSerializer()); module.addSerializer(DateFieldImpl.class, new BasicFieldSerializer()); module.addSerializer(BooleanFieldImpl.class, new BasicFieldSerializer()); + module.addSerializer(JsonFieldImpl.class, new BasicFieldSerializer()); module.addSerializer(FieldList.class, new FieldListSerializer()); module.addSerializer(JsonObject.class, new JsonObjectSerializer()); module.addSerializer(JsonArray.class, new JsonArraySerializer()); + module.addSerializer(JsonContent.class, new JsonContentSerializer()); module.addSerializer(FieldMapImpl.class, new JsonSerializer() { @Override @@ -123,6 +155,7 @@ public void serialize(FieldMapImpl value, JsonGenerator gen, SerializerProvider module.addDeserializer(FieldSchema.class, new FieldSchemaDeserializer()); module.addDeserializer(EventCauseInfo.class, new EventCauseInfoDeserializer()); module.addDeserializer(PermissionChangedEventModel.class, new PermissionChangedEventModelDeserializer()); + module.addDeserializer(JsonContent.class, new JsonContentDeserializer()); defaultMapper.registerModule(module); defaultMapper.registerModule(new SimpleModule("interfaceMapping") { @@ -253,6 +286,35 @@ public static String getJsonSchema(Class clazz) { } } + /** + * Create new schema validator against the given input schema. + * + * @param schema + * @return + */ + public static Validator newJsonSchemaValidator(JsonSchema schema) { + return Validator.create(schema, new JsonSchemaOptions().setBaseUri("https://gentics.com/mesh").setDraft(Draft.DRAFT202012)); + } + + /** + * Compare two {@link RestModel} implementor instances. + * + * @param + * @param + * @param a + * @param b + * @return + */ + public static boolean equals(A a, B b) { + if (a == null && b == null) { + return true; + } + if (a != null && b != null) { + return new JsonObject(toJson(a)).equals(new JsonObject(toJson(b))); + } + return false; + } + /** * Return the JSON object mapper. * diff --git a/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonContentDeserializer.java b/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonContentDeserializer.java new file mode 100644 index 0000000000..02a19ebb64 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonContentDeserializer.java @@ -0,0 +1,22 @@ +package com.gentics.mesh.json.deserializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.gentics.mesh.core.rest.node.field.JsonContent; + +public class JsonContentDeserializer extends JsonDeserializer { + + @Override + public JsonContent deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException { + ObjectCodec oc = p.getCodec(); + JsonNode node = oc.readTree(p); + return JsonContent.fromString(node.toString()); + } + +} diff --git a/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonObjectDeserializer.java b/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonObjectDeserializer.java index b0b09e10ef..0e9b0c83fe 100644 --- a/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonObjectDeserializer.java +++ b/rest-model/src/main/java/com/gentics/mesh/json/deserializer/JsonObjectDeserializer.java @@ -22,5 +22,4 @@ public JsonObject deserialize(JsonParser jsonParser, DeserializationContext ctxt JsonNode node = oc.readTree(jsonParser); return new JsonObject(node.toString()); } - } diff --git a/rest-model/src/main/java/com/gentics/mesh/json/serializer/BasicFieldSerializer.java b/rest-model/src/main/java/com/gentics/mesh/json/serializer/BasicFieldSerializer.java index ce262a74f5..dfe50d3ad6 100644 --- a/rest-model/src/main/java/com/gentics/mesh/json/serializer/BasicFieldSerializer.java +++ b/rest-model/src/main/java/com/gentics/mesh/json/serializer/BasicFieldSerializer.java @@ -11,11 +11,13 @@ import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.Field; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.NumberField; import com.gentics.mesh.core.rest.node.field.StringField; import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.schema.FieldSchema; @@ -69,6 +71,14 @@ public void serialize(T value, JsonGenerator gen, SerializerProvider serializers gen.writeBoolean(booleanField.getValue()); } break; + case JSON: + JsonField jsonField = (JsonFieldImpl) value; + if (jsonField.getJson() == null) { + gen.writeNull(); + } else { + gen.writePOJO(jsonField.getJson()); + } + break; case DATE: DateField dateField = (DateFieldImpl) value; if (dateField.getDate() == null) { diff --git a/rest-model/src/main/java/com/gentics/mesh/json/serializer/JsonContentSerializer.java b/rest-model/src/main/java/com/gentics/mesh/json/serializer/JsonContentSerializer.java new file mode 100644 index 0000000000..669746eb77 --- /dev/null +++ b/rest-model/src/main/java/com/gentics/mesh/json/serializer/JsonContentSerializer.java @@ -0,0 +1,19 @@ +package com.gentics.mesh.json.serializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.gentics.mesh.core.rest.node.field.JsonContent; + +/** + * Custom JSON serializer for Vert.x {@link JsonContent}. + */ +public class JsonContentSerializer extends JsonSerializer { + + @Override + public void serialize(JsonContent value, JsonGenerator jgen, SerializerProvider serializers) throws IOException { + jgen.writeObject(value.isArray() ? value.getArray() : value.getObject()); + } +} diff --git a/tests/common/src/main/java/com/gentics/mesh/assertj/impl/MicronodeResponseAssert.java b/tests/common/src/main/java/com/gentics/mesh/assertj/impl/MicronodeResponseAssert.java index fb5fb84e06..f9210bee29 100644 --- a/tests/common/src/main/java/com/gentics/mesh/assertj/impl/MicronodeResponseAssert.java +++ b/tests/common/src/main/java/com/gentics/mesh/assertj/impl/MicronodeResponseAssert.java @@ -5,6 +5,7 @@ import org.assertj.core.api.AbstractAssert; +import com.gentics.mesh.core.rest.common.FieldTypes; import com.gentics.mesh.core.rest.micronode.MicronodeResponse; import com.gentics.mesh.core.rest.node.field.Field; import com.gentics.mesh.core.rest.node.field.list.FieldList; @@ -26,41 +27,47 @@ public MicronodeResponseAssert matches(MicronodeResponse expected, MicroschemaMo for (FieldSchema fieldSchema : schema.getFields()) { String key = fieldSchema.getName(); - switch (fieldSchema.getType()) { - case "html": + switch (FieldTypes.valueByName(fieldSchema.getType())) { + case HTML: assertThat(expected.getFields().getHtmlField(key)).isNotNull(); assertThat(actual.getFields().getHtmlField(key).getHTML()).as("Field " + key) .isEqualTo(expected.getFields().getHtmlField(key).getHTML()); break; - case "binary": + case MICRONODE: + case S3BINARY: + case BINARY: break; - case "boolean": + case BOOLEAN: assertThat(expected.getFields().getBooleanField(key)).isNotNull(); assertThat(actual.getFields().getBooleanField(key).getValue()).as("Field " + key) .isEqualTo(expected.getFields().getBooleanField(key).getValue()); break; - case "date": + case DATE: assertThat(expected.getFields().getDateField(key)).isNotNull(); assertThat(actual.getFields().getDateField(key).getDate()).as("Field " + key) .isEqualTo(expected.getFields().getDateField(key).getDate()); break; - case "node": + case NODE: assertThat(expected.getFields().getNodeField(key)).isNotNull(); assertThat(actual.getFields().getNodeField(key).getUuid()).as("Field " + key) .isEqualTo(expected.getFields().getNodeField(key).getUuid()); break; - case "string": + case STRING: assertThat(expected.getFields().getStringField(key)).isNotNull(); assertThat(actual.getFields().getStringField(key).getString()).as("Field " + key) .isEqualTo(expected.getFields().getStringField(key).getString()); break; - case "number": + case NUMBER: assertThat(expected.getFields().getNumberField(key)).isNotNull(); assertThat(actual.getFields().getNumberField(key).getNumber().doubleValue()).as("Field " + key) .isEqualTo(expected.getFields().getNumberField(key).getNumber().doubleValue()); break; - case "list": - + case JSON: + assertThat(expected.getFields().getJsonField(key)).isNotNull(); + assertThat(actual.getFields().getJsonField(key).getJson()).as("Field " + key) + .isEqualTo(expected.getFields().getJsonField(key).getJson()); + break; + case LIST: Field field = actual.getFields().getField(key, fieldSchema); if (field instanceof NodeFieldList) { // compare list of nodes by comparing their uuids @@ -72,9 +79,8 @@ public MicronodeResponseAssert matches(MicronodeResponse expected, MicroschemaMo // assertThat(((FieldList) field).getItems()) // .containsExactlyElementsOf(expected.getFields().get(key, FieldList.class).getItems()); } - + break; } - } return this; diff --git a/tests/common/src/main/java/com/gentics/mesh/test/context/TestHelper.java b/tests/common/src/main/java/com/gentics/mesh/test/context/TestHelper.java index 569dd3dccb..0ecbe5dad9 100644 --- a/tests/common/src/main/java/com/gentics/mesh/test/context/TestHelper.java +++ b/tests/common/src/main/java/com/gentics/mesh/test/context/TestHelper.java @@ -83,6 +83,7 @@ import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; @@ -1015,6 +1016,8 @@ static FieldSchema fieldIntoSchema(Field field) { return new BinaryFieldSchemaImpl(); case BOOLEAN: return new BooleanFieldSchemaImpl(); + case JSON: + return new JsonFieldSchemaImpl(); case DATE: return new DateFieldSchemaImpl(); case HTML: diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/AbstractComparatorJsonTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/AbstractComparatorJsonTest.java new file mode 100644 index 0000000000..79b0031dba --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/AbstractComparatorJsonTest.java @@ -0,0 +1,72 @@ +package com.gentics.mesh.core.data.fieldhandler; + +import static com.gentics.mesh.assertj.MeshAssertions.assertThat; +import static com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeOperation.UPDATEFIELD; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.List; + +import org.junit.Test; + +import com.gentics.mesh.FieldUtil; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; +import com.gentics.mesh.core.rest.schema.FieldSchemaContainer; +import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel; + +public abstract class AbstractComparatorJsonTest extends AbstractSchemaComparatorTest { + + @Override + public JsonFieldSchema createField(String fieldName) { + return FieldUtil.createJsonFieldSchema("test"); + } + + @Test + @Override + public void testSameField() throws IOException { + C containerA = createContainer(); + containerA.setName("test"); + C containerB = createContainer(); + containerB.setName("test"); + + JsonFieldSchema fieldA = FieldUtil.createJsonFieldSchema("test"); + fieldA.setLabel("label1"); + fieldA.setRequired(true); + containerA.addField(fieldA); + + JsonFieldSchema fieldB = FieldUtil.createJsonFieldSchema("test"); + fieldB.setRequired(true); + fieldB.setLabel("label1"); + containerB.addField(fieldB); + + List changes = getComparator().diff(containerA, containerB); + assertThat(changes).hasSize(0); + } + + @Test + @Override + public void testUpdateField() throws IOException { + C containerA = createContainer(); + containerA.setName("test"); + C containerB = createContainer(); + containerB.setName("test"); + + JsonFieldSchema fieldA = FieldUtil.createJsonFieldSchema("test"); + fieldA.setLabel("label1"); + fieldA.setRequired(true); + containerA.addField(fieldA); + + JsonFieldSchema fieldB = FieldUtil.createJsonFieldSchema("test"); + fieldB.setLabel("label2"); + containerB.addField(fieldB); + + // required flag: + fieldB.setRequired(false); + List changes = getComparator().diff(containerA, containerB); + assertThat(changes).hasSize(1); + assertThat(changes.get(0)).is(UPDATEFIELD).forField("test").hasProperty("required", false).hasProperty("label", "label2"); + assertThat(changes.get(0).getProperties()).hasSize(3); + + } + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/microschema/MicroschemaComparatorJsonTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/microschema/MicroschemaComparatorJsonTest.java new file mode 100644 index 0000000000..89b8ccf968 --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/microschema/MicroschemaComparatorJsonTest.java @@ -0,0 +1,25 @@ +package com.gentics.mesh.core.data.fieldhandler.microschema; + +import static com.gentics.mesh.test.TestSize.FULL; + +import com.gentics.mesh.FieldUtil; +import com.gentics.mesh.core.data.fieldhandler.AbstractComparatorJsonTest; +import com.gentics.mesh.core.data.schema.handler.AbstractFieldSchemaContainerComparator; +import com.gentics.mesh.core.data.schema.handler.MicroschemaComparatorImpl; +import com.gentics.mesh.core.rest.schema.MicroschemaModel; +import com.gentics.mesh.test.MeshTestSetting; + +@MeshTestSetting(testSize = FULL, startServer = false) +public class MicroschemaComparatorJsonTest extends AbstractComparatorJsonTest { + + @Override + public AbstractFieldSchemaContainerComparator getComparator() { + return new MicroschemaComparatorImpl(); + } + + @Override + public MicroschemaModel createContainer() { + return FieldUtil.createMinimalValidMicroschema(); + } + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/schema/SchemaComparatorJsonTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/schema/SchemaComparatorJsonTest.java new file mode 100644 index 0000000000..7eaf1862bd --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/data/fieldhandler/schema/SchemaComparatorJsonTest.java @@ -0,0 +1,25 @@ +package com.gentics.mesh.core.data.fieldhandler.schema; + +import static com.gentics.mesh.test.TestSize.FULL; + +import com.gentics.mesh.FieldUtil; +import com.gentics.mesh.core.data.fieldhandler.AbstractComparatorJsonTest; +import com.gentics.mesh.core.data.schema.handler.AbstractFieldSchemaContainerComparator; +import com.gentics.mesh.core.data.schema.handler.SchemaComparatorImpl; +import com.gentics.mesh.core.rest.schema.SchemaModel; +import com.gentics.mesh.test.MeshTestSetting; + +@MeshTestSetting(testSize = FULL, startServer = false) +public class SchemaComparatorJsonTest extends AbstractComparatorJsonTest { + + @Override + public AbstractFieldSchemaContainerComparator getComparator() { + return new SchemaComparatorImpl(); + } + + @Override + public SchemaModel createContainer() { + return FieldUtil.createMinimalValidSchema(); + } + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/FieldSchemaCreator.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/FieldSchemaCreator.java index 17df40ba5f..d96283721c 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/FieldSchemaCreator.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/FieldSchemaCreator.java @@ -34,6 +34,8 @@ public interface FieldSchemaCreator { public final static FieldSchemaCreator CREATENUMBERLIST = name -> FieldUtil.createListFieldSchema(name, "number"); public final static FieldSchemaCreator CREATESTRING = name -> FieldUtil.createStringFieldSchema(name); public final static FieldSchemaCreator CREATESTRINGLIST = name -> FieldUtil.createListFieldSchema(name, "string"); + public final static FieldSchemaCreator CREATEJSON = name -> FieldUtil.createJsonFieldSchema(name); + public final static FieldSchemaCreator CREATEJSONLIST = name -> FieldUtil.createListFieldSchema(name, "json"); FieldSchema create(String name); } diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldEndpointTest.java new file mode 100644 index 0000000000..65f1ef6096 --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldEndpointTest.java @@ -0,0 +1,224 @@ +package com.gentics.mesh.core.field.json; + +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; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; + +import com.gentics.mesh.core.data.HibNodeFieldContainer; +import com.gentics.mesh.core.data.dao.ContentDao; +import com.gentics.mesh.core.data.node.HibNode; +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.field.AbstractFieldEndpointTest; +import com.gentics.mesh.core.rest.JsonSchema; +import com.gentics.mesh.core.rest.node.NodeResponse; +import com.gentics.mesh.core.rest.node.field.Field; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; +import com.gentics.mesh.core.rest.schema.SchemaVersionModel; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; +import com.gentics.mesh.json.JsonUtil; +import com.gentics.mesh.test.MeshTestSetting; +import com.gentics.mesh.test.TestSize; +import com.gentics.mesh.util.VersionNumber; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.json.JsonObject; + + +@MeshTestSetting(testSize = TestSize.PROJECT_AND_NODE, startServer = true) +public class JsonFieldEndpointTest extends AbstractFieldEndpointTest { + + private static final String FIELD_NAME = "jsonField"; + + /** + * Update the schema and add a json field. + * + * @throws IOException + */ + @Before + public void updateSchema() throws IOException { + try (Tx tx = tx()) { + + // add non restricted json field + JsonFieldSchema jsonFieldSchema = new JsonFieldSchemaImpl(); + jsonFieldSchema.setName(FIELD_NAME); + jsonFieldSchema.setLabel("Some label"); + + // add restricted json field + JsonFieldSchema restrictedJsonFieldSchema = new JsonFieldSchemaImpl(); + restrictedJsonFieldSchema.setName("restrictedjsonField"); + restrictedJsonFieldSchema.setLabel("Some label"); + restrictedJsonFieldSchema.setAllowedSchemas(new JsonSchema("{\"type\":\"object\",\"properties\":{\"firstName\":{\"type\":\"string\"},\"lastName\":{\"type\":\"string\"}},\"required\":[\"firstName\",\"lastName\"]}")); + + prepareTypedSchema(schemaContainer("folder"), List.of(jsonFieldSchema, restrictedJsonFieldSchema), Optional.empty()); + tx.success(); + } + } + + @Test + @Override + public void testCreateNodeWithNoField() { + NodeResponse response = createNode(null, (Field) null); + JsonFieldImpl jsonField = response.getFields().getJsonField(FIELD_NAME); + assertNull(jsonField); + } + + @Test + @Override + public void testUpdateNodeFieldWithField() { + for (int i = 0; i < 20; i++) { + VersionNumber oldVersion = tx(tx -> { return tx.contentDao().getFieldContainer(folder("2015"), "en").getVersion(); }); + + JsonContent newValue = JsonFieldTestHelper.make("content " + i); + + NodeResponse response = updateNode(FIELD_NAME, new JsonFieldImpl().setJson(newValue)); + JsonFieldImpl field = response.getFields().getJsonField(FIELD_NAME); + assertEquals(newValue, field.getJson()); + assertEquals("Check version number", oldVersion.nextDraft().toString(), response.getVersion()); + } + } + + @Test + @Override + public void testUpdateSameValue() { + NodeResponse firstResponse = updateNode(FIELD_NAME, new JsonFieldImpl().setJson(JsonFieldTestHelper.make("bla"))); + String oldNumber = firstResponse.getVersion(); + + NodeResponse secondResponse = updateNode(FIELD_NAME, new JsonFieldImpl().setJson(JsonFieldTestHelper.make("bla"))); + assertThat(secondResponse.getVersion()).as("New version number").isEqualTo(oldNumber); + } + + @Test + @Override + public void testUpdateSetNull() { + disableAutoPurge(); + + JsonContent old = JsonFieldTestHelper.make("bla"); + NodeResponse firstResponse = updateNode(FIELD_NAME, new JsonFieldImpl().setJson(old)); + String oldVersion = firstResponse.getVersion(); + + NodeResponse secondResponse = updateNode(FIELD_NAME, null); + assertThat(secondResponse.getFields().getJsonField(FIELD_NAME)).as("Updated Field").isNull(); + assertThat(secondResponse.getVersion()).as("New version number").isNotEqualTo(oldVersion); + + // Assert that the old version was not modified + try (Tx tx = tx()) { + ContentDao contentDao = tx.contentDao(); + HibNode node = folder("2015"); + HibNodeFieldContainer latest = contentDao.getLatestDraftFieldContainer(node, english()); + assertThat(latest.getVersion().toString()).isEqualTo(secondResponse.getVersion()); + assertThat(latest.getJson(FIELD_NAME)).isNull(); + assertThat(latest.getPreviousVersion().getJson(FIELD_NAME)).isNotNull(); + JsonContent oldValue = latest.getPreviousVersion().getJson(FIELD_NAME).getJson(); + assertThat(oldValue).isEqualTo(old); + } + NodeResponse thirdResponse = updateNode(FIELD_NAME, null); + assertEquals("The field does not change and thus the version should not be bumped.", thirdResponse.getVersion(), + secondResponse.getVersion()); + } + + @Test + @Override + public void testUpdateSetEmpty() { + JsonContent content = JsonFieldTestHelper.make("bla"); + NodeResponse firstResponse = updateNode(FIELD_NAME, new JsonFieldImpl().setJson(content)); + JsonField emptyField = new JsonFieldImpl(); + emptyField.setJson(null); + String oldVersion = firstResponse.getVersion(); + NodeResponse secondResponse = updateNode(FIELD_NAME, emptyField); + assertThat(secondResponse.getFields().getDateField(FIELD_NAME)).as("Field Value").isNull(); + assertThat(secondResponse.getVersion()).as("New version number").isNotEqualTo(oldVersion); + } + + /** + * Get the json value + * + * @param container + * container + * @param fieldName + * field name + * @return json value (may be null) + */ + protected JsonContent getJsonValue(HibNodeFieldContainer container, String fieldName) { + HibJsonField field = container.getJson(fieldName); + return field != null ? field.getJson() : null; + } + + @Test + @Override + public void testCreateNodeWithField() { + NodeResponse response = createNodeWithField(); + JsonFieldImpl field = response.getFields().getJsonField(FIELD_NAME); + assertEquals(JsonFieldTestHelper.make("someJson"), field.getJson()); + } + + @Test + @Override + public void testReadNodeWithExistingField() { + JsonContent someJson = JsonFieldTestHelper.make("someJson"); + try (Tx tx = tx()) { + HibNode node = folder("2015"); + ContentDao contentDao = tx.contentDao(); + + HibNodeFieldContainer container = contentDao.createFieldContainer(node, english(), + node.getProject().getLatestBranch(), user(), + contentDao.getLatestDraftFieldContainer(node, english()), true); + HibJsonField jsonField = container.createJson(FIELD_NAME); + jsonField.setJson(someJson); + tx.success(); + } + NodeResponse response = readNode(folder("2015")); + JsonFieldImpl deserializedJsonField = response.getFields().getJsonField(FIELD_NAME); + assertNotNull(deserializedJsonField); + assertEquals(someJson, deserializedJsonField.getJson()); + } + + @Test + public void testValueRestrictionValidValue() { + JsonContent valid = JsonContent.fromObject(new JsonObject().put("firstName", "Mickey").put("lastName", "Mouse")); + NodeResponse response = updateNode("restrictedjsonField", new JsonFieldImpl().setJson(valid)); + JsonFieldImpl field = response.getFields().getJsonField("restrictedjsonField"); + assertEquals(valid, field.getJson()); + } + + @Test + public void testValueRestrictionInvalidValue() { + JsonContent invalid = JsonFieldTestHelper.make("whatever"); + updateNodeFailure("restrictedjsonField", new JsonFieldImpl().setJson(invalid), HttpResponseStatus.BAD_REQUEST, + "node_error_invalid_json_field_value", "restrictedjsonField", JsonUtil.toJson(invalid)); + } + + @Test + public void testValueRemoveValueRestrictions() { + try (Tx tx = tx()) { + SchemaVersionModel schema = schemaContainer("folder").getLatestVersion().getSchema(); + + // unrestrict json field + JsonFieldSchema restrictedJsonFieldSchema = schema.getField("restrictedjsonField", JsonFieldSchema.class); + restrictedJsonFieldSchema.setAllowedSchemas(); + + schemaContainer("folder").getLatestVersion().setSchema(schema); + tx.success(); + } + JsonContent valid = JsonFieldTestHelper.make("whatever"); + NodeResponse response = updateNode("restrictedjsonField", new JsonFieldImpl().setJson(valid)); + JsonFieldImpl field = response.getFields().getJsonField("restrictedjsonField"); + assertEquals(valid, field.getJson()); + } + + @Override + public NodeResponse createNodeWithField() { + return createNode(FIELD_NAME, new JsonFieldImpl().setJson(JsonFieldTestHelper.make("someJson"))); + } +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldListEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldListEndpointTest.java new file mode 100644 index 0000000000..6b62e284bb --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldListEndpointTest.java @@ -0,0 +1,235 @@ +package com.gentics.mesh.core.field.json; + +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.make; +import static com.gentics.mesh.test.TestDataProvider.PROJECT_NAME; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; +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; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Test; + +import com.gentics.mesh.core.data.HibNodeFieldContainer; +import com.gentics.mesh.core.data.dao.ContentDao; +import com.gentics.mesh.core.data.node.HibNode; +import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.field.AbstractListFieldEndpointTest; +import com.gentics.mesh.core.rest.node.NodeResponse; +import com.gentics.mesh.core.rest.node.field.Field; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; +import com.gentics.mesh.test.MeshTestSetting; +import com.gentics.mesh.test.TestSize; + +@MeshTestSetting(testSize = TestSize.PROJECT_AND_NODE, startServer = true) +public class JsonFieldListEndpointTest extends AbstractListFieldEndpointTest { + + protected static final List TEST_LIST = List.of(make("A"), make("B"), make("C")); + + @Override + public String getListFieldType() { + return "json"; + } + + @Test + @Override + public void testCreateNodeWithField() { + NodeResponse response = createNodeWithField(); + JsonFieldListImpl field = response.getFields().getJsonFieldList(FIELD_NAME); + assertThat(field.getItems()).as("Only valid values should be stored").containsExactlyElementsOf(TEST_LIST); + } + + @Test + @Override + public void testNullValueInListOnCreate() { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.add(make("A")); + listField.add(make("B")); + listField.add(null); + createNodeAndExpectFailure(FIELD_NAME, listField, BAD_REQUEST, "field_list_error_null_not_allowed", FIELD_NAME); + } + + @Test + @Override + public void testNullValueInListOnUpdate() { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.add(make("A")); + listField.add(make("B")); + listField.add(null); + updateNodeFailure(FIELD_NAME, listField, BAD_REQUEST, "field_list_error_null_not_allowed", FIELD_NAME); + } + + @Test + @Override + public void testCreateNodeWithNoField() { + NodeResponse response = createNode(FIELD_NAME, (Field) null); + assertThat(response.getFields().getJsonFieldList(FIELD_NAME)).as("List field in reponse should be null").isNull(); + } + + @Test + @Override + public void testUpdateSameValue() { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.setItems(TEST_LIST); + + NodeResponse firstResponse = updateNode(FIELD_NAME, listField); + String oldVersion = firstResponse.getVersion(); + + NodeResponse secondResponse = updateNode(FIELD_NAME, listField); + assertThat(secondResponse.getVersion()).as("New version number").isEqualTo(oldVersion); + } + + @Test + @Override + public void testReadNodeWithExistingField() { + // 1. Update an existing node + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.setItems(TEST_LIST); + NodeResponse firstResponse = updateNode(FIELD_NAME, listField); + + // 2. Read the node + NodeResponse response = readNode(PROJECT_NAME, firstResponse.getUuid()); + JsonFieldListImpl deserializedField = response.getFields().getJsonFieldList(FIELD_NAME); + assertNotNull(deserializedField); + assertThat(deserializedField.getItems()).as("List field values from updated node").containsExactlyElementsOf(TEST_LIST); + } + + @Test + public void testCreateNodeWithNullFieldValue() throws IOException { + NodeResponse response = createNode(FIELD_NAME, (Field) null); + JsonFieldListImpl nodeField = response.getFields().getJsonFieldList(FIELD_NAME); + assertNull("No json field should have been created.", nodeField); + } + + @Test + public void testCreateEmptyStringList() throws IOException { + JsonFieldListImpl listField = new JsonFieldListImpl(); + NodeResponse response = createNode(FIELD_NAME, listField); + JsonFieldListImpl listFromResponse = response.getFields().getJsonFieldList(FIELD_NAME); + assertEquals(0, listFromResponse.getItems().size()); + } + + @Test + public void testCreateNullStringList() throws IOException { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.setItems(null); + NodeResponse response = createNode(FIELD_NAME, listField); + JsonFieldListImpl listFromResponse = response.getFields().getJsonFieldList(FIELD_NAME); + assertNull("The json list should be null since the request was sending null instead of an array.", listFromResponse); + } + + @Test + public void testJsonList() throws IOException { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.setItems(TEST_LIST); + + NodeResponse response = createNode(FIELD_NAME, listField); + JsonFieldListImpl listFromResponse = response.getFields().getJsonFieldList(FIELD_NAME); + assertEquals(3, listFromResponse.getItems().size()); + assertEquals(TEST_LIST.toString(), listFromResponse.getItems().toString()); + } + + @Test + @Override + public void testUpdateNodeFieldWithField() throws IOException { + disableAutoPurge(); + HibNode node = folder("2015"); + + List> valueCombinations = Arrays.asList(Arrays.asList("A", "B", "C"), Arrays.asList("C", "B", "A"), Collections.emptyList(), + Arrays.asList("X", "Y"), Arrays.asList("C")); + + HibNodeFieldContainer container = tx(tx -> { return tx.contentDao().getFieldContainer(node, "en"); }); + for (int i = 0; i < 20; i++) { + JsonFieldListImpl list = new JsonFieldListImpl(); + List oldValue; + List newValue; + try (Tx tx = tx()) { + oldValue = getListValues(container::getJsonList, FIELD_NAME); + newValue = valueCombinations.get(i % valueCombinations.size()).stream().map(JsonFieldTestHelper::make).collect(Collectors.toList()); + + for (JsonContent value : newValue) { + list.add(value); + } + } + NodeResponse response = updateNode(FIELD_NAME, list); + JsonFieldListImpl field = response.getFields().getJsonFieldList(FIELD_NAME); + assertThat(field.getItems()).as("Updated field").containsExactlyElementsOf(list.getItems()); + + try (Tx tx = tx()) { + ContentDao contentDao = tx.contentDao(); + HibNodeFieldContainer newContainerVersion = contentDao.getNextVersions(container).iterator().next(); + assertEquals("The old container version did not match", container.getVersion().nextDraft().toString(), + response.getVersion().toString()); + assertEquals("Check version number", newContainerVersion.getVersion().toString(), response.getVersion()); + assertEquals("Check old value", oldValue, getListValues(container::getJsonList, FIELD_NAME)); + assertEquals("Check new value", newValue, getListValues(newContainerVersion::getJsonList, FIELD_NAME)); + container = newContainerVersion; + } + } + } + + @Test + @Override + public void testUpdateSetNull() { + disableAutoPurge(); + + JsonFieldListImpl list = new JsonFieldListImpl(); + list.setItems(TEST_LIST); + NodeResponse firstResponse = updateNode(FIELD_NAME, list); + String oldVersion = firstResponse.getVersion(); + + NodeResponse secondResponse = updateNode(FIELD_NAME, null); + assertThat(secondResponse.getFields().getJsonFieldList(FIELD_NAME)).as("Updated Field").isNull(); + assertThat(oldVersion).as("Version should be updated").isNotEqualTo(secondResponse.getVersion()); + + // Assert that the old version was not modified + try (Tx tx = tx()) { + ContentDao contentDao = tx.contentDao(); + HibNode node = folder("2015"); + HibNodeFieldContainer latest = contentDao.getLatestDraftFieldContainer(node, english()); + assertThat(latest.getVersion().toString()).isEqualTo(secondResponse.getVersion()); + assertThat(latest.getJsonList(FIELD_NAME)).isNull(); + assertThat(latest.getPreviousVersion().getJsonList(FIELD_NAME)).isNotNull(); + List oldValueList = latest.getPreviousVersion().getJsonList(FIELD_NAME).getList().stream().map(item -> item.getJson()) + .collect(Collectors.toList()); + assertThat(oldValueList).containsExactlyElementsOf(TEST_LIST); + } + NodeResponse thirdResponse = updateNode(FIELD_NAME, null); + assertEquals("The field does not change and thus the version should not be bumped.", thirdResponse.getVersion(), + secondResponse.getVersion()); + } + + @Test + @Override + public void testUpdateSetEmpty() { + JsonFieldListImpl list = new JsonFieldListImpl(); + list.setItems(TEST_LIST); + NodeResponse firstResponse = updateNode(FIELD_NAME, list); + String oldVersion = firstResponse.getVersion(); + + JsonFieldListImpl emptyField = new JsonFieldListImpl(); + NodeResponse secondResponse = updateNode(FIELD_NAME, emptyField); + assertThat(secondResponse.getFields().getJsonFieldList(FIELD_NAME)).as("Updated field list").isNotNull(); + assertThat(secondResponse.getFields().getJsonFieldList(FIELD_NAME).getItems()).as("Field value should be truncated").isEmpty(); + assertThat(secondResponse.getVersion()).as("New version number should be generated").isNotEqualTo(oldVersion); + + NodeResponse thirdResponse = updateNode(FIELD_NAME, emptyField); + assertEquals("The field does not change and thus the version should not be bumped.", thirdResponse.getVersion(), secondResponse.getVersion()); + assertThat(secondResponse.getVersion()).as("No new version number should be generated").isEqualTo(secondResponse.getVersion()); + } + + @Override + public NodeResponse createNodeWithField() { + JsonFieldListImpl listField = new JsonFieldListImpl(); + listField.setItems(TEST_LIST); + + return createNode(FIELD_NAME, listField); + } +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTest.java new file mode 100644 index 0000000000..f65569377b --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTest.java @@ -0,0 +1,242 @@ +package com.gentics.mesh.core.field.json; + +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.CREATE_EMPTY; +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.FETCH; +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.FILLTEXT; +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.make; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.gentics.mesh.context.InternalActionContext; +import com.gentics.mesh.core.data.HibNodeFieldContainer; +import com.gentics.mesh.core.data.dao.ContentDao; +import com.gentics.mesh.core.data.node.HibNode; +import com.gentics.mesh.core.data.node.field.HibJsonField; +import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.field.AbstractFieldTest; +import com.gentics.mesh.core.rest.node.NodeResponse; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; +import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; +import com.gentics.mesh.json.JsonUtil; +import com.gentics.mesh.test.MeshTestSetting; +import com.gentics.mesh.test.TestSize; +import com.gentics.mesh.test.context.NoConsistencyCheck; +import com.gentics.mesh.util.CoreTestUtils; + +import io.vertx.core.json.JsonObject; + +@MeshTestSetting(testSize = TestSize.PROJECT_AND_NODE, startServer = false) +public class JsonFieldTest extends AbstractFieldTest { + + private static final String JSON_FIELD = "jsonField"; + + @Override + protected JsonFieldSchema createFieldSchema(boolean isRequired) { + return createFieldSchema(JSON_FIELD, isRequired); + } + protected JsonFieldSchema createFieldSchema(String fieldKey, boolean isRequired) { + JsonFieldSchema schema = new JsonFieldSchemaImpl(); + schema.setLabel("Some JSON field"); + schema.setRequired(isRequired); + schema.setName(fieldKey); + return schema; + } + + @Test + @Override + public void testFieldTransformation() throws Exception { + try (Tx tx = tx()) { + ContentDao contentDao = tx.contentDao(); + HibNode node = folder("2015"); + + // Add a new string field to the schema + JsonFieldSchemaImpl jsonFieldSchema = new JsonFieldSchemaImpl(); + jsonFieldSchema.setName(JSON_FIELD); + jsonFieldSchema.setLabel("Some string field"); + jsonFieldSchema.setRequired(true); + prepareTypedSchema(node, jsonFieldSchema, false); + tx.commit(); + + HibNodeFieldContainer container = contentDao.createFieldContainer(node, english(), + node.getProject().getLatestBranch(), user(), + contentDao.getLatestDraftFieldContainer(node, english()), true); + HibJsonField field = container.createJson(JSON_FIELD); + field.setJson(make("someString")); + tx.success(); + } + + try (Tx tx = tx()) { + HibNode node = folder("2015"); + String json = getJson(node); + assertTrue("The json should contain the string but it did not.{" + json + "}", json.indexOf("someString") > 1); + assertNotNull(json); + NodeResponse response = JsonUtil.readValue(json, NodeResponse.class); + assertNotNull(response); + + com.gentics.mesh.core.rest.node.field.JsonField deserializedNodeField = response.getFields().getJsonField(JSON_FIELD); + assertNotNull(deserializedNodeField); + assertEquals(make("someString"), deserializedNodeField.getJson()); + } + } + + @Test + @NoConsistencyCheck + @Override + public void testClone() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonField testField = container.createJson(JSON_FIELD); + testField.setJson(JsonContent.fromObject(new JsonObject())); + + HibNodeFieldContainer otherContainer = CoreTestUtils.createContainer(createFieldSchema(true)); + testField.cloneTo(otherContainer); + + assertThat(otherContainer.getJson(JSON_FIELD)).as("cloned field").isNotNull().isEqualToIgnoringGivenFields(testField, + "parentContainer"); + } + } + + @Test + @Override + public void testFieldUpdate() throws Exception { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonField jsonField = container.createJson(JSON_FIELD); + assertEquals(JSON_FIELD, jsonField.getFieldKey()); + jsonField.setJson(make("dummyString")); + assertEquals(make("dummyString"), jsonField.getJson()); + HibJsonField bogusField1 = container.getJson("bogus"); + assertNull(bogusField1); + HibJsonField reloadedJsonField = container.getJson(JSON_FIELD); + assertNotNull(reloadedJsonField); + assertEquals(JSON_FIELD, reloadedJsonField.getFieldKey()); + } + } + + @Test + @Override + public void testEquals() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true), createFieldSchema(JSON_FIELD + "_2", false)); + String testValue = "test123"; + HibJsonField fieldA = container.createJson(JSON_FIELD); + HibJsonField fieldB = container.createJson(JSON_FIELD + "_2"); + fieldA.setJson(make(testValue)); + fieldB.setJson(make(testValue)); + assertTrue("Both fields should be equal to eachother", fieldA.equals(fieldB)); + } + } + + @Test + @Override + public void testEqualsNull() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true), createFieldSchema(JSON_FIELD + "_2", false)); + HibJsonField fieldA = container.createJson(JSON_FIELD); + HibJsonField fieldB = container.createJson(JSON_FIELD + "_2"); + assertTrue("Both fields should be equal to eachother", fieldA.equals(fieldB)); + } + } + + @SuppressWarnings("unlikely-arg-type") + @Test + @Override + public void testEqualsRestField() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + String dummyValue = "test123"; + + // rest null - graph null + HibJsonField fieldA = container.createJson(JSON_FIELD); + JsonFieldImpl restField = new JsonFieldImpl(); + assertTrue("Both fields should be equal to eachother since both values are null", fieldA.equals(restField)); + + // rest set - graph set - different values + fieldA.setJson(make(dummyValue)); + restField.setJson(make(dummyValue + 1L)); + assertFalse("Both fields should be different since both values are not equal", fieldA.equals(restField)); + + // rest set - graph set - same value + restField.setJson(make(dummyValue)); + assertTrue("Both fields should be equal since values are equal", fieldA.equals(restField)); + + // rest set - graph set - same value different type + assertFalse("Fields should not be equal since the type does not match.", fieldA.equals(new HtmlFieldImpl().setHTML(JsonUtil.toJson(make(dummyValue))))); + } + } + + @Test + @Override + public void testUpdateFromRestNullOnCreate() { + try (Tx tx = tx()) { + invokeUpdateFromRestTestcase(JSON_FIELD, FETCH, CREATE_EMPTY); + } + } + + @Test + @Override + public void testUpdateFromRestNullOnCreateRequired() { + try (Tx tx = tx()) { + invokeUpdateFromRestNullOnCreateRequiredTestcase(JSON_FIELD, FETCH); + } + } + + @Test + @Override + public void testRemoveFieldViaNull() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeRemoveFieldViaNullTestcase(JSON_FIELD, FETCH, FILLTEXT, (node) -> { + updateContainer(ac, node, JSON_FIELD, null); + }); + } + } + + @Test + @Override + public void testRemoveRequiredFieldViaNull() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeRemoveRequiredFieldViaNullTestcase(JSON_FIELD, FETCH, FILLTEXT, (container) -> { + updateContainer(ac, container, JSON_FIELD, null); + }); + } + } + + @Test + public void testRemoveSegmentField() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeRemoveSegmentFieldViaNullTestcase(JSON_FIELD, FETCH, FILLTEXT, (container) -> { + updateContainer(ac, container, JSON_FIELD, null); + }); + } + } + + @Test + @Override + public void testUpdateFromRestValidSimpleValue() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeUpdateFromRestValidSimpleValueTestcase(JSON_FIELD, FILLTEXT, (container) -> { + JsonField field = new JsonFieldImpl(); + field.setJson(make("someValue")); + updateContainer(ac, container, JSON_FIELD, field); + }, (container) -> { + HibJsonField field = container.getJson(JSON_FIELD); + assertNotNull("The graph field {" + JSON_FIELD + "} could not be found.", field); + assertEquals("The string of the field was not updated.", make("someValue"), field.getJson()); + }); + } + } +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTestHelper.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTestHelper.java new file mode 100644 index 0000000000..49f86d1350 --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonFieldTestHelper.java @@ -0,0 +1,20 @@ +package com.gentics.mesh.core.field.json; + +import com.gentics.mesh.core.field.DataProvider; +import com.gentics.mesh.core.field.FieldFetcher; +import com.gentics.mesh.core.rest.node.field.JsonContent; + +import io.vertx.core.json.JsonObject; + +public interface JsonFieldTestHelper { + + static final String EXAMPLE_DATE = "2011-12-03T10:15:30Z"; + + static final DataProvider FILLTEXT = (container, name) -> container.createJson(name).setJson(make("whatever")); + static final DataProvider CREATE_EMPTY = (container, name) -> container.createJson(name).setJson(null); + static final FieldFetcher FETCH = (container, name) -> container.getJson(name); + + public static JsonContent make(String content) { + return JsonContent.fromObject(new JsonObject().put("content", content)); + } +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTest.java new file mode 100644 index 0000000000..1ea56112f9 --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTest.java @@ -0,0 +1,254 @@ +package com.gentics.mesh.core.field.json; + +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.make; +import static com.gentics.mesh.core.field.json.JsonListFieldTestHelper.CREATE_EMPTY; +import static com.gentics.mesh.core.field.json.JsonListFieldTestHelper.FETCH; +import static com.gentics.mesh.core.field.json.JsonListFieldTestHelper.FILLTEXT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.commons.collections.CollectionUtils; +import org.junit.Test; + +import com.gentics.mesh.context.InternalActionContext; +import com.gentics.mesh.core.data.HibNodeFieldContainer; +import com.gentics.mesh.core.data.dao.ContentDao; +import com.gentics.mesh.core.data.node.HibNode; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; +import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.field.AbstractFieldTest; +import com.gentics.mesh.core.rest.node.NodeResponse; +import com.gentics.mesh.core.rest.node.field.Field; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; +import com.gentics.mesh.core.rest.schema.ListFieldSchema; +import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; +import com.gentics.mesh.test.MeshTestSetting; +import com.gentics.mesh.test.TestSize; +import com.gentics.mesh.test.context.NoConsistencyCheck; +import com.gentics.mesh.util.CoreTestUtils; + +@MeshTestSetting(testSize = TestSize.PROJECT_AND_NODE, startServer = false) +public class JsonListFieldTest extends AbstractFieldTest { + + private static final String JSON_LIST = "jsonList"; + + @Override + protected ListFieldSchema createFieldSchema(boolean isRequired) { + return createFieldSchema(JSON_LIST, isRequired); + } + protected ListFieldSchema createFieldSchema(String fieldKey, boolean isRequired) { + ListFieldSchema schema = new ListFieldSchemaImpl(); + schema.setListType("json"); + schema.setName(fieldKey); + schema.setRequired(isRequired); + return schema; + } + + @Test + @Override + public void testFieldTransformation() throws Exception { + try (Tx tx = tx()) { + HibNode node = folder("2015"); + ContentDao contentDao = tx.contentDao(); + prepareNode(node, JSON_LIST, "json"); + HibNodeFieldContainer container = contentDao.createFieldContainer(node, english(), + node.getProject().getLatestBranch(), user(), + contentDao.getLatestDraftFieldContainer(node, english()), true); + HibJsonFieldList jsonList = container.createJsonList(JSON_LIST); + jsonList.createJson(make("dummyString1")); + jsonList.createJson(make("dummyString2")); + tx.success(); + } + + try (Tx tx = tx()) { + HibNode node = folder("2015"); + NodeResponse response = transform(node); + assertList(2, JSON_LIST, "json", response); + } + } + + @Test + @Override + public void testFieldUpdate() throws Exception { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonFieldList list = container.createJsonList(JSON_LIST); + + list.createJson(make("1")); + assertEquals(JSON_LIST, list.getFieldKey()); + assertNotNull(list.getList()); + + assertEquals(1, list.getList().size()); + assertEquals(list.getSize(), list.getList().size()); + list.createJson(make("2")); + assertEquals(2, list.getList().size()); + list.createJson(make("3")).setJson(make("Some json 3")); + assertEquals(3, list.getList().size()); + assertEquals(make("Some json 3"), list.getList().get(2).getJson()); + + HibJsonFieldList loadedList = container.getJsonList(JSON_LIST); + assertNotNull(loadedList); + assertEquals(3, loadedList.getSize()); + list.removeAll(); + assertEquals(0, list.getSize()); + assertEquals(0, list.getList().size()); + } + } + + @Test + @Override + public void testBulkFieldUpdate() throws Exception { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonFieldList list = container.createJsonList(JSON_LIST); + List params = List.of("1","2","3","4","whatever").stream().map(JsonFieldTestHelper::make).collect(Collectors.toList()); + list.createJsons(params); + assertEquals(5, list.getSize()); + assertEquals(5, list.getList().size()); + assertTrue(CollectionUtils.isEqualCollection(params, list.getValues())); + } + } + + @Test + @NoConsistencyCheck + @Override + public void testClone() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonFieldList testField = container.createJsonList(JSON_LIST); + testField.createJson(make("one")); + testField.createJson(make("two")); + testField.createJson(make("three")); + + HibNodeFieldContainer otherContainer = CoreTestUtils.createContainer(createFieldSchema(true)); + testField.cloneTo(otherContainer); + + assertThat(otherContainer.getJsonList(JSON_LIST).equals(testField)); + } + } + + @Test + @Override + public void testEquals() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema("fieldA", true), createFieldSchema("fieldB", true)); + HibJsonFieldList fieldA = container.createJsonList("fieldA"); + HibJsonFieldList fieldB = container.createJsonList("fieldB"); + assertTrue("The field should be equal to itself", fieldA.equals(fieldA)); + fieldA.addItem(fieldA.createJson(make("testString"))); + assertTrue("The field should still be equal to itself", fieldA.equals(fieldA)); + + assertFalse("The field should not be equal to a non-json field", fieldA.equals("bogus")); + assertFalse("The field should not be equal since fieldB has no value", fieldA.equals(fieldB)); + fieldB.addItem(fieldB.createJson(make("testString"))); + assertTrue("Both fields have the same value and should be equal", fieldA.equals(fieldB)); + } + } + + @Test + @Override + public void testEqualsNull() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + HibJsonFieldList fieldA = container.createJsonList(JSON_LIST); + assertFalse(fieldA.equals((Field) null)); + assertFalse(fieldA.equals((HibJsonFieldList) null)); + } + } + + @Test + @Override + public void testEqualsRestField() { + try (Tx tx = tx()) { + HibNodeFieldContainer container = CoreTestUtils.createContainer(createFieldSchema(true)); + String dummyValue = "test123"; + + // rest null - graph null + HibJsonFieldList fieldA = container.createJsonList(JSON_LIST); + + JsonFieldListImpl restField = new JsonFieldListImpl(); + assertTrue("Both fields should be equal to eachother since both values are null", fieldA.equals(restField)); + + // rest set - graph set - different values + fieldA.addItem(fieldA.createJson(make(dummyValue))); + restField.add(make(dummyValue + 1L)); + assertFalse("Both fields should be different since both values are not equal", fieldA.equals(restField)); + + // rest set - graph set - same value + restField.getItems().clear(); + restField.add(make(dummyValue)); + assertTrue("Both fields should be equal since values are equal", fieldA.equals(restField)); + + HtmlFieldListImpl otherTypeRestField = new HtmlFieldListImpl(); + otherTypeRestField.add(dummyValue); + // rest set - graph set - same value different type + assertFalse("Fields should not be equal since the type does not match.", fieldA.equals(otherTypeRestField)); + } + } + + @Test + @Override + public void testUpdateFromRestNullOnCreate() { + try (Tx tx = tx()) { + invokeUpdateFromRestTestcase(JSON_LIST, FETCH, CREATE_EMPTY); + } + } + + @Test + @Override + public void testUpdateFromRestNullOnCreateRequired() { + try (Tx tx = tx()) { + invokeUpdateFromRestNullOnCreateRequiredTestcase(JSON_LIST, FETCH); + } + } + + @Test + @Override + public void testRemoveFieldViaNull() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeRemoveFieldViaNullTestcase(JSON_LIST, FETCH, FILLTEXT, (node) -> { + updateContainer(ac, node, JSON_LIST, null); + }); + } + } + + @Test + @Override + public void testRemoveRequiredFieldViaNull() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeRemoveRequiredFieldViaNullTestcase(JSON_LIST, FETCH, FILLTEXT, (container) -> { + updateContainer(ac, container, JSON_LIST, null); + }); + } + } + + @Override + public void testUpdateFromRestValidSimpleValue() { + try (Tx tx = tx()) { + InternalActionContext ac = mockActionContext(); + invokeUpdateFromRestValidSimpleValueTestcase(JSON_LIST, FILLTEXT, (container) -> { + JsonFieldListImpl field = new JsonFieldListImpl(); + field.getItems().add(make("someValue")); + field.getItems().add(make("someValue2")); + updateContainer(ac, container, JSON_LIST, field); + }, (container) -> { + HibJsonFieldList field = container.getJsonList(JSON_LIST); + assertNotNull("The graph field {" + JSON_LIST + "} could not be found.", field); + assertEquals("The list of the field was not updated.", 2, field.getList().size()); + assertEquals("The list item of the field was not updated.", make("someValue"), field.getList().get(0).getJson()); + assertEquals("The list item of the field was not updated.", make("someValue2"), field.getList().get(1).getJson()); + }); + } + } + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTestHelper.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTestHelper.java new file mode 100644 index 0000000000..8320beb79f --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/field/json/JsonListFieldTestHelper.java @@ -0,0 +1,40 @@ +package com.gentics.mesh.core.field.json; + +import static com.gentics.mesh.core.field.json.JsonFieldTestHelper.make; + +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; +import com.gentics.mesh.core.field.DataProvider; +import com.gentics.mesh.core.field.FieldFetcher; + +public interface JsonListFieldTestHelper { + + static final String TEXT1 = "one"; + + static final String TEXT2 = "two"; + + static final String TEXT3 = "three"; + + static final DataProvider FILLTEXT = (container, name) -> { + HibJsonFieldList field = container.createJsonList(name); + field.createJson(make(TEXT1)); + field.createJson(make(TEXT2)); + field.createJson(make(TEXT3)); + }; + + static final DataProvider FILLNUMBERS = (container, name) -> { + HibJsonFieldList field = container.createJsonList(name); + field.createJson(make("1")); + field.createJson(make("0")); + }; + + static final DataProvider FILLTRUEFALSE = (container, name) -> { + HibJsonFieldList field = container.createJsonList(name); + field.createJson(make("true")); + field.createJson(make("false")); + }; + + static final DataProvider CREATE_EMPTY = (container, name) -> container.createJsonList(name); + + static final FieldFetcher FETCH = (container, name) -> container.getJsonList(name); + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java index 495b112226..1d95324888 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java @@ -39,6 +39,7 @@ import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -60,6 +61,7 @@ import com.gentics.mesh.core.rest.node.NodeResponse; import com.gentics.mesh.core.rest.node.NodeUpdateRequest; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.StringField; import com.gentics.mesh.core.rest.node.field.image.FocalPoint; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; @@ -70,6 +72,7 @@ import com.gentics.mesh.core.rest.schema.BooleanFieldSchema; import com.gentics.mesh.core.rest.schema.DateFieldSchema; import com.gentics.mesh.core.rest.schema.HtmlFieldSchema; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; import com.gentics.mesh.core.rest.schema.NodeFieldSchema; @@ -80,6 +83,7 @@ import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; @@ -220,10 +224,12 @@ protected static Stream> queries() { Arrays.asList("filtering/nodes-datelist-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-htmllist-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-node-field-native", true, false, "draft"), + Arrays.asList("filtering/nodes-json-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-micronode-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-binary-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-s3binary-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-nodelist-field-native", true, false, "draft"), + Arrays.asList("filtering/nodes-jsonlist-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-micronodelist-field-native", true, false, "draft"), Arrays.asList("filtering/nodes-paged", true, false, "draft") ); @@ -258,6 +264,7 @@ public void testNodeQuery() throws Exception { microschemaRequest.addField(FieldUtil.createStringFieldSchema("text")); microschemaRequest.addField(FieldUtil.createNodeFieldSchema("nodeRef").setAllowedSchemas("content")); microschemaRequest.addField(FieldUtil.createListFieldSchema("nodeList", "node")); + microschemaRequest.addField(FieldUtil.createJsonFieldSchema("json")); MicroschemaResponse microschemaResponse = call(() -> client.createMicroschema(microschemaRequest)); microschemaUuid = microschemaResponse.getUuid(); call(() -> client.assignMicroschemaToProject(PROJECT_NAME, microschemaResponse.getUuid())); @@ -340,6 +347,10 @@ public void testNodeQuery() throws Exception { stringLinkFieldSchema.setName("stringLink"); schema.addField(stringLinkFieldSchema); + JsonFieldSchema jsonFieldSchema = new JsonFieldSchemaImpl(); + jsonFieldSchema.setName("json"); + schema.addField(jsonFieldSchema); + BooleanFieldSchema booleanFieldSchema = new BooleanFieldSchemaImpl(); booleanFieldSchema.setName("boolean"); schema.addField(booleanFieldSchema); @@ -364,6 +375,11 @@ public void testNodeQuery() throws Exception { htmlListSchema.setName("htmlList"); schema.addField(htmlListSchema); + ListFieldSchema jsonListSchema = new ListFieldSchemaImpl(); + jsonListSchema.setListType("json"); + jsonListSchema.setName("jsonList"); + schema.addField(jsonListSchema); + ListFieldSchema booleanListSchema = new ListFieldSchemaImpl(); booleanListSchema.setListType("boolean"); booleanListSchema.setName("booleanList"); @@ -419,6 +435,14 @@ public void testNodeQuery() throws Exception { // stringLink container.createString("stringLink").setString("Link: {{mesh.link(\"" + CONTENT_UUID + "\", \"en\")}}"); + // json + container.createJson("json").setJson(JsonContent.fromObject(new JsonObject(""" + { + "firstName": "Mickey", + "lastName": "Mouse" + } + """))); + // boolean container.createBoolean("boolean").setBoolean(true); @@ -439,6 +463,21 @@ public void testNodeQuery() throws Exception { stringList.createString("C"); stringList.createString("D Link: {{mesh.link(\"" + CONTENT_UUID + "\", \"en\")}}"); + // jsonList + HibJsonFieldList jsonList = container.createJsonList("jsonList"); + jsonList.createJson(JsonContent.fromObject(new JsonObject(""" + { + "firstName": "Minnie", + "lastName": "Mouse" + } + """))); + jsonList.createJson(JsonContent.fromObject(new JsonObject(""" + { + "firstName": "Daisy", + "lastName": "Duck" + } + """))); + // htmlList HibHtmlFieldList htmlList = container.createHTMLList("htmlList"); htmlList.createHTML("A"); @@ -481,6 +520,13 @@ public void testNodeQuery() throws Exception { HibMicronode secondMicronode = micronodeList.createMicronode(microschemaDao.findByUuid(microschemaUuid).getLatestVersion()); secondMicronode.createString("text").setString("Joe"); secondMicronode.createNode("nodeRef", content()); + secondMicronode.createJson("json").setJson(JsonContent.fromObject(new JsonObject(""" + { + "firstName":"Donald", + "lastName": "Duck" + } + """))); + HibNodeFieldList micrnodeNodeList = secondMicronode.createNodeList("nodeList"); micrnodeNodeList.createNode(0, node2); micrnodeNodeList.createNode(1, node3); @@ -505,6 +551,7 @@ public void testNodeQuery() throws Exception { secondMicronode = micronodeList.createMicronode(microschemaDao.findByUuid(microschemaUuid).getLatestVersion()); secondMicronode.createString("text").setString("Jane"); secondMicronode.createNode("nodeRef", content()); + micrnodeNodeList = secondMicronode.createNodeList("nodeList"); micrnodeNodeList.createNode(0, node2); micrnodeNodeList.createNode(1, node3); diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/javafilter/JavaGraphQLEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/javafilter/JavaGraphQLEndpointTest.java index 313680627f..d18e7c2857 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/javafilter/JavaGraphQLEndpointTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/javafilter/JavaGraphQLEndpointTest.java @@ -37,6 +37,7 @@ public static Stream> queries() { Arrays.asList("filtering/roles-java", true, false, "draft"), Arrays.asList("filtering/nodes-string-field-java", true, false, "draft"), Arrays.asList("filtering/nodes-number-field-java", true, false, "draft"), + Arrays.asList("filtering/nodes-json-field-java", true, false, "draft"), Arrays.asList("filtering/nodes-nodereferences-java", true, false, "draft") ); } diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/change/FieldSchemaContainerMutatorTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/change/FieldSchemaContainerMutatorTest.java index 0d9b7b4fd7..c82317688b 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/change/FieldSchemaContainerMutatorTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/change/FieldSchemaContainerMutatorTest.java @@ -10,6 +10,9 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.util.Map; + import org.junit.Test; import com.gentics.mesh.core.data.schema.HibFieldTypeChange; @@ -19,10 +22,13 @@ import com.gentics.mesh.core.data.schema.handler.FieldSchemaContainerMutator; import com.gentics.mesh.core.db.CommonTx; import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.rest.JsonSchema; +import com.gentics.mesh.core.rest.JsonSchemaType; import com.gentics.mesh.core.rest.schema.BinaryFieldSchema; import com.gentics.mesh.core.rest.schema.BooleanFieldSchema; import com.gentics.mesh.core.rest.schema.DateFieldSchema; import com.gentics.mesh.core.rest.schema.HtmlFieldSchema; +import com.gentics.mesh.core.rest.schema.JsonFieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; import com.gentics.mesh.core.rest.schema.MicronodeFieldSchema; import com.gentics.mesh.core.rest.schema.NodeFieldSchema; @@ -35,6 +41,7 @@ import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.NodeFieldSchemaImpl; @@ -45,6 +52,7 @@ import com.gentics.mesh.test.MeshTestSetting; import com.gentics.mesh.test.context.AbstractMeshTest; import com.gentics.mesh.util.IndexOptionHelper; +import com.hazelcast.jet.json.JsonUtil; /** * Test for common mutator operations on a field containers. @@ -134,7 +142,7 @@ public void testUpdateLabel() throws MeshSchemaException { } @Test - public void testAUpdateFields() throws MeshSchemaException { + public void testAUpdateFields() throws MeshSchemaException, IOException { try (Tx tx = tx()) { CommonTx ctx = tx.unwrap(); @@ -160,6 +168,12 @@ public void testAUpdateFields() throws MeshSchemaException { nodeField.setRequired(true); schemaModel.addField(nodeField); + JsonFieldSchema jsonField = new JsonFieldSchemaImpl(); + jsonField.setAllowedSchemas(new JsonSchema().setProperties(Map.of("blub", new JsonSchemaType())).setRequired(new String[] {"blub"})); + jsonField.setName("jsonField"); + jsonField.setRequired(true); + schemaModel.addField(jsonField); + MicronodeFieldSchema micronodeField = new MicronodeFieldSchemaImpl(); micronodeField.setAllowedMicroSchemas("blub"); micronodeField.setName("micronodeField"); @@ -208,12 +222,19 @@ public void testAUpdateFields() throws MeshSchemaException { nodeFieldUpdate.setRestProperty(SchemaChangeModel.REQUIRED_KEY, false); binaryFieldUpdate.setNextChange(nodeFieldUpdate); + HibUpdateFieldChange jsonFieldUpdate = (HibUpdateFieldChange) ctx.schemaDao().createPersistedChange(version, SchemaChangeOperation.UPDATEFIELD); + jsonFieldUpdate.setRestProperty(ALLOW_KEY, new String[] { JsonUtil.toJson(new JsonSchema().setProperties(Map.of("content", new JsonSchemaType())).setRequired(new String[] {"content"})) }); + jsonFieldUpdate.setFieldName("jsonField"); + jsonFieldUpdate.setRestProperty(SchemaChangeModel.REQUIRED_KEY, false); + jsonFieldUpdate.setIndexOptions(IndexOptionHelper.getRawFieldOption()); + nodeFieldUpdate.setNextChange(jsonFieldUpdate); + HibUpdateFieldChange stringFieldUpdate = (HibUpdateFieldChange) ctx.schemaDao().createPersistedChange(version, SchemaChangeOperation.UPDATEFIELD); stringFieldUpdate.setRestProperty(ALLOW_KEY, new String[] { "valueA", "valueB" }); stringFieldUpdate.setFieldName("stringField"); stringFieldUpdate.setRestProperty(SchemaChangeModel.REQUIRED_KEY, false); stringFieldUpdate.setIndexOptions(IndexOptionHelper.getRawFieldOption()); - nodeFieldUpdate.setNextChange(stringFieldUpdate); + jsonFieldUpdate.setNextChange(stringFieldUpdate); HibUpdateFieldChange htmlFieldUpdate = (HibUpdateFieldChange) ctx.schemaDao().createPersistedChange(version, SchemaChangeOperation.UPDATEFIELD); htmlFieldUpdate.setFieldName("htmlField"); @@ -280,6 +301,14 @@ public void testAUpdateFields() throws MeshSchemaException { assertTrue("The index option did not contain the raw field. {" + stringFieldSchema.getElasticsearch().encodePrettily() + "}", stringFieldSchema.getElasticsearch().containsKey("raw")); + // JSON + JsonFieldSchema jsonFieldSchema = updatedSchema.getField("jsonField", JsonFieldSchemaImpl.class); + assertNotNull(jsonFieldSchema); + assertArrayEquals(new JsonSchema[] { new JsonSchema().setProperties(Map.of("content", new JsonSchemaType())).setRequired(new String[] {"content"}) }, jsonFieldSchema.getAllowedSchemas()); + assertFalse("The required flag should now be set to false.", jsonFieldSchema.isRequired()); + assertTrue("The index option did not contain the raw field. {" + jsonFieldSchema.getElasticsearch().encodePrettily() + "}", + jsonFieldSchema.getElasticsearch().containsKey("raw")); + // Html HtmlFieldSchema htmlFieldSchema = updatedSchema.getField("htmlField", HtmlFieldSchemaImpl.class); assertNotNull(htmlFieldSchema); diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/field/json/JsonFieldEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/field/json/JsonFieldEndpointTest.java new file mode 100644 index 0000000000..a095e57d3c --- /dev/null +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/schema/field/json/JsonFieldEndpointTest.java @@ -0,0 +1,66 @@ +package com.gentics.mesh.core.schema.field.json; + +import static com.gentics.mesh.core.rest.job.JobStatus.COMPLETED; +import static com.gentics.mesh.test.ClientHelper.call; +import static com.gentics.mesh.test.ElasticsearchTestMode.TRACKING; +import static com.gentics.mesh.test.TestSize.FULL; +import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; + +import org.junit.Test; + +import com.gentics.mesh.FieldUtil; +import com.gentics.mesh.core.field.json.JsonFieldTestHelper; +import com.gentics.mesh.core.rest.JsonSchema; +import com.gentics.mesh.core.rest.node.NodeUpdateRequest; +import com.gentics.mesh.core.rest.schema.SchemaModel; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.SchemaUpdateRequest; +import com.gentics.mesh.json.JsonUtil; +import com.gentics.mesh.test.MeshTestSetting; +import com.gentics.mesh.test.context.AbstractMeshTest; + +@MeshTestSetting(elasticsearch = TRACKING, testSize = FULL, startServer = true) +public class JsonFieldEndpointTest extends AbstractMeshTest { + + @Test + public void testResetAllowField() { + grantAdmin(); + final String schemaUuid = tx(() -> schemaContainer("content").getUuid()); + final String nodeUuid = tx(() -> contentUuid()); + + // 1. Update schema and set allowed property + SchemaModel schema = tx(() -> schemaContainer("content").getLatestVersion().getSchema()); + SchemaUpdateRequest request = JsonUtil.readValue(schema.toJson(), SchemaUpdateRequest.class); + request.addField(new JsonFieldSchemaImpl() + .setAllowedSchemas(new JsonSchema("{\"type\":\"object\",\"properties\":{\"firstName\":{\"type\":\"string\"},\"lastName\":{\"type\":\"string\"}},\"required\":[\"firstName\",\"lastName\"]}")) + .setName("extraJson")); + + waitForJobs(() -> { + call(() -> client().updateSchema(schemaUuid, request)); + }, COMPLETED, 1); + + // 2. Update the node slug and expect failure due to now allowed string + NodeUpdateRequest nodeUpdateRequest = new NodeUpdateRequest(); + nodeUpdateRequest.setVersion("draft"); + nodeUpdateRequest.setLanguage("en"); + nodeUpdateRequest.getFields().put("extraJson", FieldUtil.createJsonField(JsonFieldTestHelper.make("someValue"))); + call(() -> client().updateNode(projectName(), nodeUuid, nodeUpdateRequest), BAD_REQUEST, "node_error_invalid_json_field_value", + "extraJson", + JsonUtil.toJson(JsonFieldTestHelper.make("someValue"))); + + // 3. Update the schema again with empty allowed value + request.removeField("extraJson"); + request.addField(new JsonFieldSchemaImpl() + .setAllowedSchemas(new JsonSchema("{\"type\":\"object\",\"properties\":{\"content\":{\"type\":\"string\"},\"extra\":{\"type\":\"string\"}},\"required\":[\"content\"]}")) + .setName("extraJson")); + + waitForJobs(() -> { + call(() -> client().updateSchema(schemaUuid, request)); + }, COMPLETED, 1); + + // 4. Update the node again + call(() -> client().updateNode(projectName(), nodeUuid, nodeUpdateRequest)); + + } + +} diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/webrootfield/WebRootFieldTypeTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/webrootfield/WebRootFieldTypeTest.java index e1c9ecc534..529104f796 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/webrootfield/WebRootFieldTypeTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/webrootfield/WebRootFieldTypeTest.java @@ -11,7 +11,13 @@ import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -32,17 +38,21 @@ import com.gentics.mesh.core.rest.node.field.BooleanField; import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.NumberField; import com.gentics.mesh.core.rest.node.field.StringField; import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NodeFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.list.FieldList; import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.MicronodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NumberFieldListImpl; @@ -53,6 +63,7 @@ import com.gentics.mesh.core.rest.schema.impl.BooleanFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.DateFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.HtmlFieldSchemaImpl; +import com.gentics.mesh.core.rest.schema.impl.JsonFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.ListFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicronodeFieldSchemaImpl; import com.gentics.mesh.core.rest.schema.impl.MicroschemaReferenceImpl; @@ -266,6 +277,53 @@ private void testStringList(boolean fieldShouldExist, boolean contentShouldExist testList(fieldShouldExist, contentShouldExist, FieldTypes.STRING, "val=1", "2val", "val3", "val 4"); } + + // JSON object field + + @Test + public void testJsonExists() throws IOException { + testJson(true, true); + } + + @Test + public void testJsonNotExists() throws IOException { + testJson(true, false); + } + + @Test + public void testJsonFieldNotExists() throws IOException { + testJson(false, false); + } + + private void testJson(boolean fieldShouldExist, boolean contentShouldExist) throws IOException { + JsonContent value = JsonContent.fromArray(new JsonArray(""" + ["Mickey", "Mouse"] + """)); + + Optional maybeField = fieldShouldExist + ? Optional.of(new JsonFieldSchemaImpl().setName("json_content").setLabel("JSON object content")) + : Optional.empty(); + + Optional> maybeContentSupplier = contentShouldExist ? Optional.of(node -> { + String uuid = tx(() -> node.getUuid()); + NodeResponse response = call(() -> client().findNodeByUuid(PROJECT_NAME, uuid, + new VersioningParametersImpl().draft(), new NodeParametersImpl().setResolveLinks(LinkType.SHORT))); + NodeUpdateRequest request = response.toRequest(); + JsonField field = new JsonFieldImpl(); + field.setJson(value); + request.getFields().put("json_content", field); + call(() -> client().updateNode(PROJECT_NAME, node.getUuid(), request)); + }) : Optional.empty(); + + Consumer resultsConsumer = response -> { + assertFalse(response.isPlainText()); + assertFalse(response.isBinary()); + Assert.assertEquals(JsonContent.fromString(response.getResponseAsJsonString()), value); + }; + + testField("/News/2015/News_2015.en.html", maybeField, maybeContentSupplier, resultsConsumer, false); + } + // Boolean field @Test @@ -331,6 +389,40 @@ private void testBooleanList(boolean fieldShouldExist, boolean contentShouldExis testList(fieldShouldExist, contentShouldExist, FieldTypes.BOOLEAN, false, true, false, true, false); } + // JSON object list field + + @Test + public void testJsonListExists() throws IOException { + testJsonList(true, true); + } + + @Test + public void testJsonListNotExists() throws IOException { + testJsonList(true, false); + } + + @Test + public void testJsonListFieldNotExists() throws IOException { + testJsonList(false, false); + } + + private void testJsonList(boolean fieldShouldExist, boolean contentShouldExist) throws IOException { + JsonObject[] values = new JsonObject[] { + new JsonObject().put("name", "Mickey"), + new JsonObject().put("name", "Minnie"), + new JsonObject().put("name", "Donald"), + new JsonObject().put("name", "Daisy"), + new JsonObject().put("name", "Pluto") + }; + testList(fieldShouldExist, contentShouldExist, FieldTypes.JSON, + List.of(values), Optional.of(response -> { + assertFalse(response.isPlainText()); + assertFalse(response.isBinary()); + JsonArray json = JsonUtil.readValue(response.getResponseAsJsonString(), JsonArray.class); + assertTrue(Arrays.equals(IntStream.range(0, json.size()).mapToObj(json::getJsonObject).toArray(size -> new JsonObject[size]), values)); + })); + } + // Date field @Test @@ -378,7 +470,7 @@ private void testDate(boolean fieldShouldExist, boolean contentShouldExist) thro testField("/News/2015/News_2015.en.html", maybeField, maybeContentSupplier, resultsConsumer, false); } - // Boolean list field + // Date list field @Test public void testDateListExists() throws IOException { @@ -684,6 +776,7 @@ private void testNodeList(boolean fieldShouldExist, boolean contentShouldExist) testList(fieldShouldExist, contentShouldExist, FieldTypes.NODE, list, Optional.of(asserter)); } + @SuppressWarnings("unchecked") private void testList(boolean fieldShouldExist, boolean contentShouldExist, FieldTypes type, T... listValues) throws IOException { List values = Arrays.asList(listValues); @@ -729,6 +822,9 @@ private void testList(boolean fieldShouldExist, boolean conte case MICRONODE: fieldList = (FieldList) new MicronodeFieldListImpl(); break; + case JSON: + fieldList = (FieldList) new JsonFieldListImpl(); + break; default: throw new IllegalArgumentException("Unsupported list item type: " + type.name()); } @@ -745,7 +841,7 @@ private void testList(boolean fieldShouldExist, boolean conte } else { assertFalse(response.isPlainText()); assertFalse(response.isBinary()); - JsonArray json = new JsonArray(response.getResponseAsJsonString()); + JsonArray json = JsonUtil.readValue(response.getResponseAsJsonString(), JsonArray.class); assertTrue(Arrays.equals(json.getList().toArray(new Object[values.size()]), values.toArray(new Object[values.size()]))); } diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/rest/node/FieldMapTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/rest/node/FieldMapTest.java index a5248374d2..6583f93ba5 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/rest/node/FieldMapTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/rest/node/FieldMapTest.java @@ -23,6 +23,8 @@ import com.gentics.mesh.core.rest.node.field.BooleanField; import com.gentics.mesh.core.rest.node.field.DateField; import com.gentics.mesh.core.rest.node.field.HtmlField; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.core.rest.node.field.JsonField; import com.gentics.mesh.core.rest.node.field.MicronodeField; import com.gentics.mesh.core.rest.node.field.NodeField; import com.gentics.mesh.core.rest.node.field.NumberField; @@ -31,6 +33,7 @@ import com.gentics.mesh.core.rest.node.field.impl.BooleanFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.DateFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.HtmlFieldImpl; +import com.gentics.mesh.core.rest.node.field.impl.JsonFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.NumberFieldImpl; import com.gentics.mesh.core.rest.node.field.impl.StringFieldImpl; import com.gentics.mesh.core.rest.node.field.list.FieldList; @@ -38,6 +41,7 @@ import com.gentics.mesh.core.rest.node.field.list.impl.BooleanFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.DateFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.HtmlFieldListImpl; +import com.gentics.mesh.core.rest.node.field.list.impl.JsonFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.MicronodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListImpl; import com.gentics.mesh.core.rest.node.field.list.impl.NodeFieldListItemImpl; @@ -46,6 +50,7 @@ import com.gentics.mesh.core.rest.schema.impl.StringFieldSchemaImpl; import com.gentics.mesh.json.JsonUtil; +import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; public class FieldMapTest { @@ -81,6 +86,10 @@ public void testJsonMapNullHandling() throws JsonParseException, JsonMappingExce fieldMap.put("booleanFieldNull", null); fieldMap.put("booleanFieldNullValue", new BooleanFieldImpl().setValue(null)); + fieldMap.put("jsonField", new JsonFieldImpl().setJson(JsonContent.fromArray(new JsonArray().add("whatever").add("wherever")))); + fieldMap.put("jsonFieldNull", null); + fieldMap.put("jsonFieldNullValue", new JsonFieldImpl().setJson(null)); + fieldMap.put("micronodeField", new MicronodeResponse()); fieldMap.put("micronodeFieldNull", null); @@ -139,6 +148,12 @@ public void testJsonMapNullHandling() throws JsonParseException, JsonMappingExce numberList.add(12); fieldMap.put("numberListField", numberList); + JsonFieldListImpl jsonList = new JsonFieldListImpl(); + jsonList.add(JsonContent.fromObject(new JsonObject().put("content", "A"))); + jsonList.add(JsonContent.fromObject(new JsonObject().put("content", "B"))); + jsonList.add(JsonContent.fromObject(new JsonObject().put("content", "C"))); + fieldMap.put("jsonListField", jsonList); + fieldMap.put("nulled", null); // Assert fieldmap fields @@ -161,6 +176,7 @@ public void testJsonMapNullHandling() throws JsonParseException, JsonMappingExce assertThat(jsonObject).hasNullValue("htmlFieldNull"); assertThat(jsonObject).hasNullValue("dateFieldNull"); assertThat(jsonObject).hasNullValue("stringFieldNull"); + assertThat(jsonObject).hasNullValue("jsonFieldNull"); assertNotNull(json); FieldMap fieldMapDeserialized = JsonUtil.readValue(json, FieldMap.class); @@ -199,6 +215,13 @@ private void assertMap(FieldMap fieldMap) { assertNull(fieldMap.getField("booleanFieldNullValue", FieldTypes.BOOLEAN, null, false)); assertNull("The field was explicitly set to null and should be null but it was not.", fieldMap.getBooleanField("booleanFieldNull")); + JsonField jsonField = fieldMap.getField("jsonField", FieldTypes.JSON, null, false); + assertNotNull(jsonField); + assertNotNull(fieldMap.getJsonField("jsonField")); + + assertNull(fieldMap.getField("jsonFieldNullValue", FieldTypes.JSON, null, false)); + assertNull("The field was explicitly set to null and should be null but it was not.", fieldMap.getJsonField("jsonFieldNull")); + DateField dateField = fieldMap.getField("dateField", FieldTypes.DATE, null, false); assertNotNull(dateField); assertNotNull(fieldMap.getDateField("dateField")); @@ -248,6 +271,10 @@ private void assertMap(FieldMap fieldMap) { assertNotNull(booleanList); assertEquals(3, booleanList.getItems().size()); + JsonFieldListImpl jsonList = fieldMap.getJsonFieldList("jsonListField"); + assertNotNull(jsonList); + assertEquals(3, jsonList.getItems().size()); + StringFieldListImpl stringList = fieldMap.getStringFieldList("stringListField"); assertNotNull(stringList); assertEquals(3, stringList.getItems().size()); @@ -260,7 +287,7 @@ private void assertMap(FieldMap fieldMap) { assertNotNull(micronodeList); assertEquals(3, micronodeList.getItems().size()); - assertEquals("The map did not contain the expected amount of fields.", 29, fieldMap.size()); + assertEquals("The map did not contain the expected amount of fields.", 33, fieldMap.size()); assertFalse("The map should not be empty.", fieldMap.isEmpty()); assertTrue("The string field should be within the map.", fieldMap.hasField("stringField")); diff --git a/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-java b/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-java new file mode 100644 index 0000000000..103b064ecc --- /dev/null +++ b/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-java @@ -0,0 +1,87 @@ +{ + jsonSchema: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json:{hasSchema: "{\"type\":\"object\",\"properties\":{\"firstName\":{\"type\":\"string\"},\"lastName\":{\"type\":\"string\"}},\"required\":[\"firstName\",\"lastName\"]}"} + } + } + }) { + # [$.data.jsonSchema.elements.length()=1] + elements { + uuid + schema { + name + } + } + } + + noJsonSchema: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json:{hasSchema: "{\"type\":\"object\",\"properties\":{\"whatever\":{\"type\":\"string\"},\"whenever\":{\"type\":\"string\"}},\"required\":[\"whatever\",\"whenever\"]}"} + } + } + }) { + # [$.data.noJsonSchema.elements.length()=0] + elements { + uuid + schema { + name + } + } + } + + incompleteJsonSchema: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json:{hasSchema: "{\"type\":\"object\",\"properties\":{\"firstName\":{\"type\":\"string\"},\"lastName\":{\"type\":\"string\"},\"middleName\":{\"type\":\"string\"}},\"required\":[\"firstName\",\"lastName\",\"middleName\"]}"} + } + } + }) { + # [$.data.incompleteJsonSchema.elements.length()=0] + elements { + uuid + schema { + name + } + } + } + + jsonPathFound: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json:{jsonPath: "$[?(@.firstName == 'Mickey')]"} + } + } + }) { + # [$.data.jsonPathFound.elements.length()=1] + elements { + uuid + schema { + name + } + } + } + + jsonPathNotFound: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json:{jsonPath: "$[?(@.firstName == 'Donald')]"} + } + } + }) { + # [$.data.jsonPathNotFound.elements.length()=0] + elements { + uuid + schema { + name + } + } + } +} +# [$.errors=] \ No newline at end of file diff --git a/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-native b/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-native new file mode 100644 index 0000000000..48f3119a3c --- /dev/null +++ b/tests/tests-core/src/main/resources/graphql/filtering/nodes-json-field-native @@ -0,0 +1,90 @@ +{ + jsonContainsMouse: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json: {like: "%Mouse%"} + } + } + }) { + # [$.data.jsonContainsMouse.elements.length()=1] + elements { + ...output + } + } + notJsonContainsMouse: nodes(filter: { + schema: {is: folder} + not: { + fields: { + folder: { + json: {like: "%Mouse%"} + } + } + } + }) { + # [$.data.notJsonContainsMouse.elements.length()=8] + elements { + ...output + } + } + + jsonIsNotNull: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json: {isNull: false} + } + } + }) { + # [$.data.jsonIsNotNull.elements.length()=1] + elements { + ...output + } + } + notJsonIsNotNull: nodes(filter: { + schema: {is: folder} + not: { + fields: { + folder: { + json: {isNull: false} + } + } + } + }) { + # [$.data.notJsonIsNotNull.elements.length()=8] + elements { + ...output + } + } + jsonIsNull: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json: {isNull: true} + } + } + }) { + # [$.data.jsonIsNull.elements.length()=8] + elements { + ...output + } + } + notJsonIsNull: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + json: {isNull: true} + } + } + }) { + # [$.data.notJsonIsNull.elements.length()=8] + elements { + ...output + } + } +} +# [$.errors=] + +fragment output on Node { + uuid +} \ No newline at end of file diff --git a/tests/tests-core/src/main/resources/graphql/filtering/nodes-jsonlist-field-native b/tests/tests-core/src/main/resources/graphql/filtering/nodes-jsonlist-field-native new file mode 100644 index 0000000000..a384f83a00 --- /dev/null +++ b/tests/tests-core/src/main/resources/graphql/filtering/nodes-jsonlist-field-native @@ -0,0 +1,93 @@ +{ + anyMatch: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + jsonList: { + anyMatch: {like: "{\"firstName\":\"Minnie\",\"lastName\":\"Mouse\"}"} + } + } + } + }) { + # [$.data.anyMatch.elements.length()=1] + elements { + uuid + schema { + name + } + } + } + allMatch: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + jsonList: { + allMatch: {like: "{\"firstName\":\"Minnie\",\"lastName\":\"Mouse\"}"} + } + } + } + }) { + # [$.data.allMatch.elements.length()=0] + elements { + uuid + schema { + name + } + } + } + noneMatch: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + jsonList: { + noneMatch: {like: "{\"firstName\":\"Darkwing\",\"lastName\":\"Duck\"}"} isNull:false + } + } + } + }) { + # [$.data.noneMatch.elements.length()=1] + elements { + uuid + schema { + name + } + } + } + anyNotMatch: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + jsonList: { + anyNotMatch: {like: "{\"firstName\":\"Darkwing\",\"lastName\":\"Duck\"}"} + } + } + } + }) { + # [$.data.anyNotMatch.elements.length()=1] + elements { + uuid + schema { + name + } + } + } + count: nodes(filter: { + schema: {is: folder} + fields: { + folder: { + jsonList: { + count: {gte: 0} + } + } + } + }) { + # [$.data.count.elements.length()=9] + elements { + uuid + schema { + name + } + } + } +} +# [$.errors=] \ No newline at end of file diff --git a/tests/tests-core/src/main/resources/graphql/node-fields-query b/tests/tests-core/src/main/resources/graphql/node-fields-query index 03627fef89..04fac4daef 100644 --- a/tests/tests-core/src/main/resources/graphql/node-fields-query +++ b/tests/tests-core/src/main/resources/graphql/node-fields-query @@ -59,6 +59,15 @@ nodeList { uuid } booleanList numberList + jsonList { + # [$.data.node.fields.jsonList[0]=] + text + } + + json { + # [$.data.node.fields.json.json.firstName=Mickey] + json + } # [$.data.node.fields.boolean=true] boolean @@ -137,6 +146,10 @@ # [$.data.node.fields.micronodeList[1].fields.nodeList[0].language=en] # [$.data.node.fields.micronodeList[1].fields.nodeList[1].language=en] language + } + json { + # [$.data.node.fields.micronodeList[1].fields.json.json.firstName=Donald] + json } } } diff --git a/tests/tests-core/src/main/resources/graphql/node-fields-query.v1 b/tests/tests-core/src/main/resources/graphql/node-fields-query.v1 index 353803cf70..5daa241152 100644 --- a/tests/tests-core/src/main/resources/graphql/node-fields-query.v1 +++ b/tests/tests-core/src/main/resources/graphql/node-fields-query.v1 @@ -64,6 +64,15 @@ nodeList { uuid } booleanList numberList + jsonList { + # [$.data.node.fields.jsonList[0]=] + text + } + + json { + # [$.data.node.fields.json.json.firstName=Mickey] + json + } # [$.data.node.fields.boolean=true] boolean @@ -114,6 +123,10 @@ # [$.data.node.fields.micronodeList[1].nodeList[1].language=en] language } + json { + # [$.data.node.fields.micronodeList[1].json.json.firstName=Donald] + json + } } } } diff --git a/verticles/graphql/pom.xml b/verticles/graphql/pom.xml index 97545e2b34..0f7f9b879b 100644 --- a/verticles/graphql/pom.xml +++ b/verticles/graphql/pom.xml @@ -45,6 +45,10 @@ com.gentics.graphqlfilter graphql-java-filter + + com.jayway.jsonpath + json-path + 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 d3799f7f4e..3b02c79ad6 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 @@ -18,6 +18,8 @@ import org.dataloader.DataLoader; import org.dataloader.DataLoaderOptions; import org.dataloader.DataLoaderRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gentics.mesh.core.db.Database; import com.gentics.mesh.core.rest.error.AbstractUnavailableException; @@ -27,6 +29,7 @@ import com.gentics.mesh.graphql.dataloader.NodeDataLoader; import com.gentics.mesh.graphql.type.QueryTypeProvider; import com.gentics.mesh.graphql.type.field.FieldDefinitionProvider; +import com.gentics.mesh.json.JsonUtil; import com.gentics.mesh.metric.MetricsService; import com.gentics.mesh.metric.SimpleMetric; @@ -40,8 +43,6 @@ import io.micrometer.core.instrument.Timer; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import io.vertx.reactivex.core.Vertx; /** @@ -110,6 +111,7 @@ public void handleQuery(GraphQLContext gc, String body) { dataLoaderRegistry.register(NodeDataLoader.NODE_REFERENCE_LOADER_KEY, DataLoader.newDataLoader(NodeDataLoader.NODE_REFERENCE_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.JSON_LIST_VALUES_DATA_LOADER_KEY, DataLoader.newDataLoader(typeProvider.getFieldDefProvider().JSON_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)); @@ -152,7 +154,7 @@ public void handleQuery(GraphQLContext gc, String body) { response.put("data", new JsonObject(data)); } boolean minify = gc.isMinify(options.getHttpServerOptions()); - gc.send(minify ? response.encode() : response.encodePrettily(), OK); + gc.send(JsonUtil.toJson(response, minify), OK); } catch (TimeoutException | InterruptedException | ExecutionException e) { // If an error happens while "waiting" for the result, we log the GraphQL query here. log.error("GraphQL query failed after {} ms with {}:\n{}\nvariables: {}", diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/FieldFilter.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/FieldFilter.java index 3dcf3900c6..97e78f3e53 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/FieldFilter.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/FieldFilter.java @@ -20,6 +20,7 @@ import com.gentics.mesh.core.data.node.field.HibBooleanField; import com.gentics.mesh.core.data.node.field.HibDateField; import com.gentics.mesh.core.data.node.field.HibHtmlField; +import com.gentics.mesh.core.data.node.field.HibJsonField; import com.gentics.mesh.core.data.node.field.HibStringField; import com.gentics.mesh.core.data.node.field.list.HibListField; import com.gentics.mesh.core.data.schema.HibFieldSchemaVersionElement; @@ -88,6 +89,9 @@ private FieldFilter(HibFieldSchemaVersionElement schemaVersion, Graph case BOOLEAN: return new FieldMappedFilter<>(type, name, description, BooleanFilter.filter(), node -> node == null ? null : getOrNull(node.getBoolean(name), HibBooleanField::getBoolean), schema); + case JSON: + return new FieldMappedFilter<>(type, name, description, JsonFilter.filter(), + node -> node == null ? null : getOrNull(node.getJson(name), HibJsonField::getJson), schema); case NUMBER: return new FieldMappedFilter<>(type, name, description, NumberFilter.filter(), node -> node == null ? null : getOrNull(node.getNumber(name), val -> new BigDecimal(val.getNumber().toString())), schema); @@ -124,6 +128,10 @@ private FieldFilter(HibFieldSchemaVersionElement schemaVersion, Graph listFilter = ListFilter.booleanListFilter(); listFieldGetter = node -> node.getBooleanList(name); break; + case JSON: + listFilter = ListFilter.jsonListFilter(); + listFieldGetter = node -> node.getJsonList(name); + break; case DATE: listFilter = ListFilter.dateListFilter(); listFieldGetter = node -> node.getDateList(name); diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/JsonFilter.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/JsonFilter.java new file mode 100644 index 0000000000..ca57519b41 --- /dev/null +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/JsonFilter.java @@ -0,0 +1,147 @@ +package com.gentics.mesh.graphql.filter; + +import static com.gentics.graphqlfilter.util.FilterUtil.nullablePredicate; +import static graphql.Scalars.GraphQLString; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gentics.graphqlfilter.filter.FilterField; +import com.gentics.graphqlfilter.filter.MainFilter; +import com.gentics.graphqlfilter.filter.operation.Comparison; +import com.gentics.mesh.core.rest.node.field.JsonContent; +import com.gentics.mesh.json.JsonUtil; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.ParseContext; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonProvider; + +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; +import io.vertx.reactivex.json.schema.JsonSchema; +import io.vertx.reactivex.json.schema.Validator; + +public class JsonFilter extends MainFilter { + + private static final Logger log = LoggerFactory.getLogger(JsonFilter.class); + + private static final String UNDERSCORE_PLACEHOLDER = "\\{UNS}\\"; + private static final String PERCENT_PLACEHOLDER = "\\{REM}\\"; + private static final String DOT_PLACEHOLDER = "\\{DOT}\\"; + private static JsonFilter instance; + + /** + * Filters JSON strings by various means + */ + public static synchronized JsonFilter filter() { + if (instance == null) { + instance = new JsonFilter(null); + } + return instance; + } + + public JsonFilter(String owner) { + super("JsonFilter", "Filters Jsons", true, Optional.ofNullable(owner)); + } + + @Override + protected List> getFilters() { + return Arrays.asList( + FilterField.isNull(), + FilterField.create("like", "Checks if the JSON object matches the given SQL LIKE expression.", GraphQLString, likePredicate(), + Optional.of((query) -> Comparison.like(query.makeFieldOperand(Optional.empty()), query.makeValueOperand(true), query.getInitiatingFilterName()))), + FilterField.create("regex", "Checks if the JSON object representation matches the given regular expression.", GraphQLString, regexPredicate(), + Optional.empty()), + FilterField.create("hasSchema", "Tests if the object has the given JSON schema.", GraphQLString, objectSchemaPredicate(), + Optional.empty()), + FilterField.create("jsonPath", "Tests the given JSON schema against JsonPath value.", GraphQLString, jsonPathPredicate(), + Optional.empty())); + } + + private Function> jsonPathPredicate() { + return query -> { + JsonPath jsonPath = JsonPath.compile(query); + JsonProvider provider = new JacksonJsonProvider(JsonUtil.getMapper()); + ParseContext context = JsonPath.using(provider); + return nullablePredicate(object -> { + Object parsed = context.parse(JsonUtil.toJson(object)).read(jsonPath); + if (parsed == null) { + return false; + } else if (parsed instanceof List list) { + return !list.isEmpty(); + } else { + return true; + } + }); + }; + } + + /** + * Parse a collection of JSON objects + * + * @param objects + * @return + */ + public static Collection parseJsons(Collection objects) { + return objects.stream().map(JsonFilter::parseJson).collect(Collectors.toList()); + } + + /** + * Try parse the given string to a JSON object, or return an empty one. + * + * @param object + * @return + */ + public static JsonContent parseJson(String object) { + try { + Object decoded = JsonUtil.readValue(object, JsonContent.class); + if (decoded instanceof JsonContent o) { + return o; + } + } catch (DecodeException e) { + log.warn("JSON decode failed for " + object, e); + } + return null; + } + + private String likeToRegex(String likeQuery) { + return likeQuery + .replace("\\.", DOT_PLACEHOLDER).replace("\\%", PERCENT_PLACEHOLDER).replace("\\_", UNDERSCORE_PLACEHOLDER) + .replace(".", "\\.").replace("%", ".*?").replace("_", ".") + .replace(DOT_PLACEHOLDER, ".").replace(PERCENT_PLACEHOLDER, "%").replace(UNDERSCORE_PLACEHOLDER, "_"); + } + + private Function> objectSchemaPredicate() { + return query -> { + JsonSchema schema = JsonSchema.of(new JsonObject(query)); + Validator validator = JsonUtil.newJsonSchemaValidator(schema); + return nullablePredicate(object -> { + Object jsonContent = object.isArray() ? object.getArray() : object.getObject(); + return validator.validate(jsonContent).getValid() == Boolean.TRUE; + }); + }; + } + + private Function> likePredicate() { + return query -> { + Pattern regex = Pattern.compile(likeToRegex(query)); + return nullablePredicate(object -> regex.matcher(JsonUtil.toJson(object)).find()); + }; + } + + private Function> regexPredicate() { + return query -> { + Pattern regex = Pattern.compile(query); + return nullablePredicate(object -> regex.matcher(JsonUtil.toJson(object)).find()); + }; + } +} diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ListFilter.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ListFilter.java index ded24c2de7..f31ada38a6 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ListFilter.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ListFilter.java @@ -30,6 +30,7 @@ import com.gentics.mesh.core.data.s3binary.S3HibBinaryField; import com.gentics.mesh.core.db.CommonTx; import com.gentics.mesh.core.db.Tx; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.graphql.context.GraphQLContext; import com.gentics.mesh.graphql.filter.operation.ListItemOperationOperand; import com.gentics.mesh.graphql.model.NodeReferenceIn; @@ -55,6 +56,7 @@ public class ListFilter extends MainFilter> { private static ListFilter htmlListFilterInstance; private static ListFilter numberListFilterInstance; private static ListFilter booleanListFilterInstance; + private static ListFilter jsonListFilterInstance; private static ListFilter dateListFilterInstance; private static ListFilter nodeListFilterInstance; private static ListFilter micronodeListFilterInstance; @@ -172,6 +174,13 @@ public boolean isReferenceItem() { return booleanListFilterInstance; } + public static final ListFilter jsonListFilter() { + if (jsonListFilterInstance == null) { + jsonListFilterInstance = new ListFilter<>("JsonListFilter", "Filters JSON object lists", JsonFilter.filter(), Optional.of("JSONLIST"), false); + } + return jsonListFilterInstance; + } + public static final ListFilter dateListFilter() { if (dateListFilterInstance == null) { dateListFilterInstance = new ListFilter<>("DateListFilter", "Filters date lists", DateFilter.filter(), Optional.of("DATELIST"), false); diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/NodeTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/NodeTypeProvider.java index eba87db209..b469d86118 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/NodeTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/NodeTypeProvider.java @@ -859,6 +859,9 @@ private List generateSchemaFieldTypesV1(GraphQLContext contex case BOOLEAN: root.field(fields.createBooleanDef(fieldSchema)); break; + case JSON: + root.field(fields.createJsonDef(fieldSchema)); + break; case NODE: root.field(fields.createNodeDef(fieldSchema)); break; @@ -924,6 +927,9 @@ private List generateSchemaFieldTypesV2(GraphQLContext contex case BOOLEAN: fieldsType.field(fields.createBooleanDef(fieldSchema)); break; + case JSON: + fieldsType.field(fields.createJsonDef(fieldSchema)); + break; case NODE: fieldsType.field(fields.createNodeDef(fieldSchema)); break; diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/QueryTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/QueryTypeProvider.java index cb8a4f8459..d880267909 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/QueryTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/QueryTypeProvider.java @@ -679,6 +679,7 @@ public GraphQLSchema getRootSchema(GraphQLContext context) { additionalTypes.add(interfaceTypeProvider.createPermInfoType()); additionalTypes.add(fieldDefProvider.createBinaryFieldType()); additionalTypes.add(fieldDefProvider.createS3BinaryFieldType()); + additionalTypes.add(fieldDefProvider.createJsonFieldType()); Versioned.doSince(2, context, () -> { List schemaFieldTypes = context.getOrStore(NodeTypeProvider.SCHEMA_FIELD_TYPES, () -> nodeTypeProvider.generateSchemaFieldTypes(context).forVersion(context)); 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 be494ce868..dcff5c4f10 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 @@ -14,7 +14,6 @@ import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; import static graphql.schema.GraphQLObjectType.newObject; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -35,7 +34,6 @@ import javax.inject.Inject; import javax.inject.Singleton; -import org.apache.commons.collections.ListUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.dataloader.BatchLoaderWithContext; @@ -54,11 +52,13 @@ import com.gentics.mesh.core.data.node.field.HibBooleanField; import com.gentics.mesh.core.data.node.field.HibDateField; import com.gentics.mesh.core.data.node.field.HibHtmlField; +import com.gentics.mesh.core.data.node.field.HibJsonField; import com.gentics.mesh.core.data.node.field.HibNumberField; import com.gentics.mesh.core.data.node.field.HibStringField; import com.gentics.mesh.core.data.node.field.list.HibBooleanFieldList; import com.gentics.mesh.core.data.node.field.list.HibDateFieldList; import com.gentics.mesh.core.data.node.field.list.HibHtmlFieldList; +import com.gentics.mesh.core.data.node.field.list.HibJsonFieldList; import com.gentics.mesh.core.data.node.field.list.HibMicronodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNodeFieldList; import com.gentics.mesh.core.data.node.field.list.HibNumberFieldList; @@ -71,6 +71,8 @@ import com.gentics.mesh.core.db.Tx; import com.gentics.mesh.core.link.WebRootLinkReplacerImpl; import com.gentics.mesh.core.rest.common.ContainerType; +import com.gentics.mesh.core.rest.common.FieldTypes; +import com.gentics.mesh.core.rest.node.field.JsonContent; import com.gentics.mesh.core.rest.node.field.image.FocalPoint; import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.ListFieldSchema; @@ -83,11 +85,12 @@ import com.gentics.mesh.graphql.filter.NodeFilter; import com.gentics.mesh.graphql.type.AbstractTypeProvider; import com.gentics.mesh.graphql.type.NodeTypeProvider; +import com.gentics.mesh.json.JsonUtil; import com.gentics.mesh.parameter.LinkType; -import com.gentics.mesh.parameter.image.CropMode; import com.gentics.mesh.util.DateUtils; import com.google.common.base.Functions; +import graphql.scalars.ExtendedScalars; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; @@ -96,11 +99,13 @@ import graphql.schema.GraphQLTypeReference; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Promise; +import io.vertx.core.json.JsonObject; @Singleton public class FieldDefinitionProvider extends AbstractTypeProvider { public static final String BINARY_FIELD_TYPE_NAME = "BinaryField"; + public static final String JSON_FIELD_TYPE_NAME = "JsonField"; public static final String S3_BINARY_FIELD_TYPE_NAME = "S3BinaryField"; /** @@ -113,6 +118,11 @@ public class FieldDefinitionProvider extends AbstractTypeProvider { */ public static final String BOOLEAN_LIST_VALUES_DATA_LOADER_KEY = "booleanListLoader"; + /** + * Key for the data loader for JSON object list field values + */ + public static final String JSON_LIST_VALUES_DATA_LOADER_KEY = "jsonListLoader"; + /** * Key for the data loader for date list field values */ @@ -207,6 +217,14 @@ private static CompletionStage>> listValueDataLoader(List> JSON_LIST_VALUE_LOADER = (keys, environment) -> { + ContentDao contentDao = Tx.get().contentDao(); + return listValueDataLoader(keys, contentDao::getJsonListFieldValues, Functions.identity()); + }; + /** * DataLoader implementation for values of date lists */ @@ -322,6 +340,21 @@ public FieldDefinitionProvider(MeshOptions options, MicronodeFieldTypeProvider m }; } + public GraphQLObjectType createJsonFieldType() { + Builder type = newObject().name(JSON_FIELD_TYPE_NAME).description("JSON object field"); + + type.field(newFieldDefinition().name("text").description("Value as JSON string").type(GraphQLString).dataFetcher(fetcher -> { + JsonContent json = fetcher.getSource(); + return json == null ? null : JsonUtil.toJson(json, options.getHttpServerOptions().isMinifyJson()); + })); + type.field(newFieldDefinition().name("json").description("Value as JSON object").type(ExtendedScalars.Json).dataFetcher(fetcher -> { + JsonContent json = fetcher.getSource(); + return json == null ? null : json; + })); + + return type.build(); + } + public GraphQLObjectType createBinaryFieldType() { Builder type = newObject().name(BINARY_FIELD_TYPE_NAME).description("Binary field"); @@ -582,6 +615,17 @@ public GraphQLFieldDefinition createBooleanDef(FieldSchema schema) { }).build(); } + public GraphQLFieldDefinition createJsonDef(FieldSchema schema) { + return newFieldDefinition().name(schema.getName()).description(schema.getLabel()).type(new GraphQLTypeReference(JSON_FIELD_TYPE_NAME)).dataFetcher(env -> { + HibFieldContainer container = env.getSource(); + HibJsonField jsonField = container.getJson(schema.getName()); + if (jsonField != null) { + return jsonField.getJson(); + } + return null; + }).build(); + } + public GraphQLFieldDefinition createNumberDef(FieldSchema schema) { return newFieldDefinition().name(schema.getName()).description(schema.getLabel()).type(GraphQLBigDecimal).dataFetcher(env -> { HibFieldContainer container = env.getSource(); @@ -672,15 +716,18 @@ public Optional createListDef(GraphQLContext context, Li NodeFilter nodeFilter = NodeFilter.filter(context); // Add link resolving arg to html and string lists - switch (schema.getListType()) { - case "html": - case "string": + switch (FieldTypes.valueByName(schema.getListType())) { + case HTML: + case STRING: + case JSON: fieldType.argument(createLinkTypeArg()); break; - case "node": + case NODE: fieldType.argument(createNodeVersionArg()); fieldType.argument(nodeFilter.createFilterArgument()); break; + default: + break; } fieldType.dataFetcher(env -> { @@ -689,8 +736,8 @@ public Optional createListDef(GraphQLContext context, Li HibFieldContainer container = env.getSource(); GraphQLContext gc = env.getContext(); - switch (schema.getListType()) { - case "boolean": + switch (FieldTypes.valueByName(schema.getListType())) { + case BOOLEAN: HibBooleanFieldList booleanList = container.getBooleanList(schema.getName()); if (booleanList == null) { return null; @@ -703,7 +750,20 @@ public Optional createListDef(GraphQLContext context, Li } else { return booleanList.getList().stream().map(item -> item.getBoolean()).collect(Collectors.toList()); } - case "html": + case JSON: + HibJsonFieldList jsonList = container.getJsonList(schema.getName()); + if (jsonList == null) { + return null; + } + + String jsonListUuid = jsonList.getUuid(); + if (contentDao.supportsPrefetchingListFieldValues() && !StringUtils.isEmpty(jsonListUuid)) { + DataLoader> jsonListValueLoader = env.getDataLoader(FieldDefinitionProvider.JSON_LIST_VALUES_DATA_LOADER_KEY); + return jsonListValueLoader.load(jsonListUuid); + } else { + return jsonList.getList(); + } + case HTML: HibHtmlFieldList htmlList = container.getHTMLList(schema.getName()); if (htmlList == null) { return null; @@ -732,7 +792,7 @@ public Optional createListDef(GraphQLContext context, Li Arrays.asList(container.getLanguageTag())); }).collect(Collectors.toList()); } - case "string": + case STRING: HibStringFieldList stringList = container.getStringList(schema.getName()); if (stringList == null) { return null; @@ -761,7 +821,7 @@ public Optional createListDef(GraphQLContext context, Li Arrays.asList(container.getLanguageTag())); }).collect(Collectors.toList()); } - case "number": + case NUMBER: HibNumberFieldList numberList = container.getNumberList(schema.getName()); if (numberList == null) { return null; @@ -774,7 +834,7 @@ public Optional createListDef(GraphQLContext context, Li } else { return numberList.getList().stream().map(item -> item.getNumber()).collect(Collectors.toList()); } - case "date": + case DATE: HibDateFieldList dateList = container.getDateList(schema.getName()); if (dateList == null) { return null; @@ -787,7 +847,7 @@ public Optional createListDef(GraphQLContext context, Li } else { return dateList.getList().stream().map(item -> DateUtils.toISO8601(item.getDate(), 0)).collect(Collectors.toList()); } - case "node": + case NODE: HibNodeFieldList nodeList = container.getNodeList(schema.getName()); if (nodeList == null) { return null; @@ -837,7 +897,7 @@ public Optional createListDef(GraphQLContext context, Li .filter(content1 -> gc.hasReadPerm(content1, nodeType)) .collect(Collectors.toList()); } - case "micronode": + case MICRONODE: HibMicronodeFieldList micronodeList = container.getMicronodeList(schema.getName()); if (micronodeList == null) { return null; @@ -858,21 +918,21 @@ public Optional createListDef(GraphQLContext context, Li } private GraphQLType getElementTypeOfList(ListFieldSchema schema) { - switch (schema.getListType()) { - case "boolean": + switch (FieldTypes.valueByName(schema.getListType())) { + case BOOLEAN: return GraphQLBoolean; - case "html": - return GraphQLString; - case "string": + case DATE: + case HTML: + case STRING: return GraphQLString; - case "number": + case NUMBER: return GraphQLBigDecimal; - case "date": - return GraphQLString; - case "node": + case NODE: return new GraphQLTypeReference(NODE_TYPE_NAME); - case "micronode": + case MICRONODE: return new GraphQLTypeReference(MICRONODE_TYPE_NAME); + case JSON: + return new GraphQLTypeReference(JSON_FIELD_TYPE_NAME); default: return null; } diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/MicronodeFieldTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/MicronodeFieldTypeProvider.java index 2612471ed4..555b724cbb 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/MicronodeFieldTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/field/MicronodeFieldTypeProvider.java @@ -19,6 +19,8 @@ import javax.inject.Singleton; import org.apache.commons.collections.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.gentics.mesh.core.data.HibElement; import com.gentics.mesh.core.data.dao.MicroschemaDao; @@ -47,8 +49,6 @@ import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeReference; import graphql.schema.GraphQLUnionType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton public class MicronodeFieldTypeProvider extends AbstractTypeProvider { @@ -162,6 +162,9 @@ public Versioned> generateMicroschemaFieldTypes(GraphQLC case BOOLEAN: microschemaType.field(fields.get().createBooleanDef(fieldSchema).transform(addDeprecation)); break; + case JSON: + microschemaType.field(fields.get().createJsonDef(fieldSchema).transform(addDeprecation)); + break; case NODE: microschemaType.field(fields.get().createNodeDef(fieldSchema).transform(addDeprecation)); break; @@ -218,6 +221,9 @@ public Versioned> generateMicroschemaFieldTypes(GraphQLC case BOOLEAN: fieldsType.field(fields.get().createBooleanDef(fieldSchema)); break; + case JSON: + fieldsType.field(fields.get().createJsonDef(fieldSchema)); + break; case NODE: fieldsType.field(fields.get().createNodeDef(fieldSchema)); break;