Repository: jhipster/generator-jhipster (ts)

Issue:
Support java.time.Duration in entity creation
##### **Overview of the feature request**

Add support for the java.time.Duration type as a field of an entity.

##### **Motivation for or Use Case**

Hibernate 5 natively supports this type, and little effort is necessary for supporting it in Mongo (a couple of converters are already defined in a companion JHipster project).

##### **Related issues or PR**

I did the homework but didn't find anything.

I am working on a PR for this. Help wanted for React! And feedback is welcome!

-   [x] Checking this box is mandatory (this is just to show you read everything)

<!-- Love JHipster? Please consider supporting our collective:
👉  https://opencollective.com/generator-jhipster/donate -->


Pull request description:
#9095: Support for java.time.Duration
use the duration to long converter instance

treat Duration as a number client-side

add a Duration field to the base test

add a missing import

consider the Duration field in the search criteria

Fix #9095

-   Please make sure the below checklist is followed for Pull Requests.

-   [ ] [Travis tests](https://travis-ci.org/jhipster/generator-jhipster/pull_requests) are green
-   [x] Tests are added where necessary
-   [x] Documentation is added/updated where necessary
-   [ ] Coding Rules & Commit Guidelines as per our [CONTRIBUTING.md document](https://github.com/jhipster/generator-jhipster/blob/master/CONTRIBUTING.md) are followed

<!--
Please also reference the issue number in a commit message to [automatically close the related Github issue](https://help.github.com/articles/closing-issues-via-commit-messages/)

Note: It is also possible to add `[skip ci]` to your commit message to skip Travis tests
-->
  

Patch:
diff --git a/generators/entity-client/templates/angular/src/main/webapp/app/entities/entity-management-update.component.html.ejs b/generators/entity-client/templates/angular/src/main/webapp/app/entities/entity-management-update.component.html.ejs
index cf58111d6b..8bbca71f92 100644
--- a/generators/entity-client/templates/angular/src/main/webapp/app/entities/entity-management-update.component.html.ejs
+++ b/generators/entity-client/templates/angular/src/main/webapp/app/entities/entity-management-update.component.html.ejs
@@ -36,7 +36,7 @@
     let fieldInputType = 'text';
     let ngModelOption = '';
     const translationKey = `${i18nKeyPrefix}.${fieldName}`;
-    if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal'].includes(fieldType)) {
+    if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'Duration'].includes(fieldType)) {
         fieldInputType = 'number';
     } else if (fieldType === 'LocalDate') {
         fieldInputType = 'date';
@@ -157,7 +157,7 @@ _%>
                             This field should follow pattern for "<%= fieldNameHumanized %>".
                         </small>
         <%_ } _%>
-        <%_ if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal'].includes(fieldType)) { _%>
+        <%_ if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'Duration'].includes(fieldType)) { _%>
                         <small class="form-text text-danger"
                             [hidden]="!editForm.get('<%= fieldName %>')?.errors?.number" jhiTranslate="entity.validation.number">
                             This field should be a number.
diff --git a/generators/entity-server/templates/src/main/java/package/domain/Entity.java.ejs b/generators/entity-server/templates/src/main/java/package/domain/Entity.java.ejs
index f83dbe6a12..424b22895d 100644
--- a/generators/entity-server/templates/src/main/java/package/domain/Entity.java.ejs
+++ b/generators/entity-server/templates/src/main/java/package/domain/Entity.java.ejs
@@ -82,6 +82,8 @@ import java.time.Instant;
 import java.time.LocalDate;
 <%_ } if (fieldsContainZonedDateTime === true) { _%>
 import java.time.ZonedDateTime;
+<%_ } if (fieldsContainDuration === true) { _%>
+import java.time.Duration;
 <%_ } if (importSet === true) { _%>
 import java.util.HashSet;
 import java.util.Set;
@@ -190,7 +192,7 @@ public class <%= asEntity(entityClass) %> implements Serializable {
         if (fieldType === 'byte[]') { _%>
     @Lob
         <%_ }
-        if (['Instant', 'ZonedDateTime', 'LocalDate'].includes(fieldType)) { _%>
+        if (['Instant', 'ZonedDateTime', 'LocalDate', 'Duration'].includes(fieldType)) { _%>
     @Column(name = "<%-fieldNameAsDatabaseColumn %>"<% if (required) { %>, nullable = false<% } %><% if (unique) { %>, unique = true<% } %>)
         <%_ } else if (fieldType === 'BigDecimal') { _%>
     @Column(name = "<%-fieldNameAsDatabaseColumn %>", precision = 21, scale = 2<% if (required) { %>, nullable = false<% } %><% if (unique) { %>, unique = true<% } %>)
diff --git a/generators/entity-server/templates/src/main/java/package/service/dto/EntityCriteria.java.ejs b/generators/entity-server/templates/src/main/java/package/service/dto/EntityCriteria.java.ejs
index 4947ff9de4..cac938ea49 100644
--- a/generators/entity-server/templates/src/main/java/package/service/dto/EntityCriteria.java.ejs
+++ b/generators/entity-server/templates/src/main/java/package/service/dto/EntityCriteria.java.ejs
@@ -1,6 +1,9 @@
 package <%= packageName %>.service.dto;
 
 import java.io.Serializable;
+<%_ if (fieldsContainDuration === true) { _%>
+import java.time.Duration;
+<%_ } _%>
 import java.util.Objects;
 <%_ for (idx in fields) { if (fields[idx].fieldIsEnum === true) { _%>
 import <%= packageName %>.domain.enumeration.<%= fields[idx].fieldType %>;
@@ -21,6 +24,9 @@ import io.github.jhipster.service.filter.InstantFilter;
 <%_ if (fieldsContainLocalDate === true) { _%>
 import io.github.jhipster.service.filter.LocalDateFilter;
 <%_ } _%>
+<%_ if (fieldsContainDuration === true) { _%>
+import io.github.jhipster.service.filter.RangeFilter;
+<%_ } _%>
 <%_ if (fieldsContainZonedDateTime === true) { _%>
 import io.github.jhipster.service.filter.ZonedDateTimeFilter;
 <%_ } _%>
@@ -36,7 +42,7 @@ import io.github.jhipster.service.filter.ZonedDateTimeFilter;
       if (field.fieldIsEnum == true) {
         filterType = fieldType + 'Filter';
         extraFilters[fieldType] = {type : filterType, superType: 'Filter<' + fieldType + '>', fieldType:fieldType};
-      } else if (['LocalDate', 'ZonedDateTime', 'Instant', 'String', 'Long', 'Integer', 'Float', 'Double', 'BigDecimal', 'Boolean'].includes(fieldType)) {
+      } else if (['Duration', 'LocalDate', 'ZonedDateTime', 'Instant', 'String', 'Long', 'Integer', 'Float', 'Double', 'BigDecimal', 'Boolean'].includes(fieldType)) {
         filterType = fieldType + 'Filter';
       } else {
         filterType = 'Filter<' + fieldType + '>';
@@ -47,6 +53,9 @@ import io.github.jhipster.service.filter.ZonedDateTimeFilter;
             fieldInJavaBeanMethod: field.fieldInJavaBeanMethod });
     }
   });
+  if (fieldsContainDuration === true) {
+    extraFilters['java.time.Duration'] = {type : 'DurationFilter', superType: 'RangeFilter<Duration>', fieldType: 'Duration'};
+  }
   relationships.forEach((relationship) => {
     const relationshipType = relationship.relationshipType;
     // user has a String PK when using OAuth, so change relationships accordingly
diff --git a/generators/entity-server/templates/src/main/java/package/service/dto/EntityDTO.java.ejs b/generators/entity-server/templates/src/main/java/package/service/dto/EntityDTO.java.ejs
index 4d14498f08..1ab8ce615b 100644
--- a/generators/entity-server/templates/src/main/java/package/service/dto/EntityDTO.java.ejs
+++ b/generators/entity-server/templates/src/main/java/package/service/dto/EntityDTO.java.ejs
@@ -37,6 +37,9 @@ import java.time.LocalDate;
 <%_ if (fieldsContainZonedDateTime) { _%>
 import java.time.ZonedDateTime;
 <%_ } _%>
+<%_ if (fieldsContainDuration) { _%>
+import java.time.Duration;
+<%_ } _%>
 <%_ if (validation) { _%>
 import javax.validation.constraints.*;
 <%_ } _%>
diff --git a/generators/entity-server/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs b/generators/entity-server/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs
index 68e2d8f990..cd83c01e10 100644
--- a/generators/entity-server/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs
+++ b/generators/entity-server/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs
@@ -95,6 +95,8 @@
                 columnType="datetime";
             } else if (fieldType === 'ZonedDateTime') {
                 columnType="datetime";
+            } else if (fieldType === 'Duration') {
+                columnType="bigint";
             } else if (fieldType === 'byte[]' && fieldTypeBlobContent !== 'text') {
                 if (prodDatabaseType === 'mysql' || prodDatabaseType === 'postgresql' || prodDatabaseType === 'mariadb') {
                     columnType="longblob";
diff --git a/generators/entity/index.js b/generators/entity/index.js
index 52fcd92bef..9f121de7c6 100644
--- a/generators/entity/index.js
+++ b/generators/entity/index.js
@@ -687,6 +687,7 @@ class EntityGenerator extends BaseBlueprintGenerator {
                 context.fieldsContainDate = false;
                 context.fieldsContainInstant = false;
                 context.fieldsContainZonedDateTime = false;
+                context.fieldsContainDuration = false;
                 context.fieldsContainLocalDate = false;
                 context.fieldsContainBigDecimal = false;
                 context.fieldsContainBlob = false;
@@ -730,6 +731,7 @@ class EntityGenerator extends BaseBlueprintGenerator {
                         'LocalDate',
                         'Instant',
                         'ZonedDateTime',
+                        'Duration',
                         'Boolean',
                         'byte[]',
                         'ByteBuffer'
@@ -808,6 +810,8 @@ class EntityGenerator extends BaseBlueprintGenerator {
                     } else if (fieldType === 'Instant') {
                         context.fieldsContainInstant = true;
                         context.fieldsContainDate = true;
+                    } else if (fieldType === 'Duration') {
+                        context.fieldsContainDuration = true;
                     } else if (fieldType === 'LocalDate') {
                         context.fieldsContainLocalDate = true;
                         context.fieldsContainDate = true;
diff --git a/generators/entity/prompts.js b/generators/entity/prompts.js
index 51ca27b3d1..e88e44842b 100644
--- a/generators/entity/prompts.js
+++ b/generators/entity/prompts.js
@@ -521,6 +521,10 @@ function askForField(done) {
                     value: 'ZonedDateTime',
                     name: 'ZonedDateTime'
                 },
+                {
+                    value: 'Duration',
+                    name: 'Duration'
+                },
                 {
                     value: 'Boolean',
                     name: 'Boolean'
@@ -713,7 +717,7 @@ function askForField(done) {
             message: 'Which validation rules do you want to add?',
             choices: response => {
                 // Default rules applicable for fieldType 'LocalDate', 'Instant',
-                // 'ZonedDateTime', 'UUID', 'Boolean', 'ByteBuffer' and 'Enum'
+                // 'ZonedDateTime', 'Duration', 'UUID', 'Boolean', 'ByteBuffer' and 'Enum'
                 const opts = [
                     {
                         name: 'Required',
diff --git a/generators/generator-base-private.js b/generators/generator-base-private.js
index 6a1db03e66..d473bf46dd 100644
--- a/generators/generator-base-private.js
+++ b/generators/generator-base-private.js
@@ -1014,7 +1014,7 @@ module.exports = class extends Generator {
                 tsType = fieldType;
             } else if (fieldType === 'Boolean') {
                 tsType = 'boolean';
-            } else if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal'].includes(fieldType)) {
+            } else if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'Duration'].includes(fieldType)) {
                 tsType = 'number';
             } else if (fieldType === 'String' || fieldType === 'UUID') {
                 tsType = 'string';
@@ -1203,7 +1203,7 @@ module.exports = class extends Generator {
      * @param {string} fieldType
      */
     getSpecificationBuilder(fieldType) {
-        if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'LocalDate', 'ZonedDateTime', 'Instant'].includes(fieldType)) {
+        if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'LocalDate', 'ZonedDateTime', 'Instant', 'Duration'].includes(fieldType)) {
             return 'buildRangeSpecification';
         }
         if (fieldType === 'String') {
diff --git a/generators/server/templates/src/main/java/package/config/CloudDatabaseConfiguration.java.ejs b/generators/server/templates/src/main/java/package/config/CloudDatabaseConfiguration.java.ejs
index 1c1b09c434..d9a3f9910e 100644
--- a/generators/server/templates/src/main/java/package/config/CloudDatabaseConfiguration.java.ejs
+++ b/generators/server/templates/src/main/java/package/config/CloudDatabaseConfiguration.java.ejs
@@ -109,6 +109,7 @@ public class CloudDatabaseConfiguration extends AbstractCloudConfig {
         List<Converter<?, ?>> converterList = new ArrayList<>();
         converterList.add(DateToZonedDateTimeConverter.INSTANCE);
         converterList.add(ZonedDateTimeToDateConverter.INSTANCE);
+        converterList.add(DurationToLongConverter.INSTANCE);
         return new MongoCustomConversions(converterList);
     }
 


Test patch:
diff --git a/generators/entity-client/templates/angular/src/test/javascript/e2e/entities/entity.spec.ts.ejs b/generators/entity-client/templates/angular/src/test/javascript/e2e/entities/entity.spec.ts.ejs
index f0687ed829..0d358d5c46 100644
--- a/generators/entity-client/templates/angular/src/test/javascript/e2e/entities/entity.spec.ts.ejs
+++ b/generators/entity-client/templates/angular/src/test/javascript/e2e/entities/entity.spec.ts.ejs
@@ -109,6 +109,8 @@ describe('<%= entityClass %> e2e test', () => {
             <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('2000-12-31'),
             <%_ } else if (['Instant', 'ZonedDateTime'].includes(fieldType)) { _%>
             <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('01/01/2001' + protractor.Key.TAB + '02:30AM'),
+            <%_ } else if (fieldType === 'Duration') { _%>
+            <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('PT12S'),
             <%_ } else if (['byte[]', 'ByteBuffer'].includes(fieldType) && fieldTypeBlobContent === 'text') { _%>
             <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('<%= fieldName %>'),
             <%_ } else if (['byte[]', 'ByteBuffer'].includes(fieldType)) { _%>
@@ -145,6 +147,8 @@ describe('<%= entityClass %> e2e test', () => {
         expect(await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input()).to.eq('2000-12-31', 'Expected <%=fieldName%> value to be equals to 2000-12-31');
             <%_ } else if (['Instant', 'ZonedDateTime'].includes(fieldType)) { _%>
         expect(await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input()).to.contain('2001-01-01T02:30', 'Expected <%=fieldName%> value to be equals to 2000-12-31');
+            <%_ } else if (fieldType === 'Duration') { _%>
+        expect(await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input()).to.contain('12', 'Expected <%=fieldName%> value to be equals to 12');
             <%_ } else if (fieldType === 'Boolean') { _%>
         const selected<%= fieldNameCapitalized %> = <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input();
         if (await selected<%= fieldNameCapitalized %>.isSelected()) {
diff --git a/generators/entity-client/templates/angular/src/test/javascript/spec/app/entities/entity-management.service.spec.ts.ejs b/generators/entity-client/templates/angular/src/test/javascript/spec/app/entities/entity-management.service.spec.ts.ejs
index 55c3ba6cde..4759e2e71c 100644
--- a/generators/entity-client/templates/angular/src/test/javascript/spec/app/entities/entity-management.service.spec.ts.ejs
+++ b/generators/entity-client/templates/angular/src/test/javascript/spec/app/entities/entity-management.service.spec.ts.ejs
@@ -75,7 +75,7 @@ describe('Service Tests', () => {
                 <%= fieldType + '.' + field.fieldValues.split(',')[0] %>,
                     <%_ } else if (fieldType === 'Boolean') { _%>
                 false,
-                    <%_ } else if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal'].includes(fieldType)) { _%>
+                    <%_ } else if (['Integer', 'Long', 'Float', 'Double', 'BigDecimal', 'Duration'].includes(fieldType)) { _%>
                 0,
                     <%_ } else if (fieldType === 'String' || fieldType === 'UUID') { _%>
                 'AAAAAAA',
diff --git a/generators/entity-client/templates/react/src/test/javascript/e2e/entities/entity.spec.ts.ejs b/generators/entity-client/templates/react/src/test/javascript/e2e/entities/entity.spec.ts.ejs
index bd9e83bf13..a9f8a63ff2 100644
--- a/generators/entity-client/templates/react/src/test/javascript/e2e/entities/entity.spec.ts.ejs
+++ b/generators/entity-client/templates/react/src/test/javascript/e2e/entities/entity.spec.ts.ejs
@@ -113,6 +113,9 @@ describe('<%= entityClass %> e2e test', () => {
             <%_ } else if (['Instant', 'ZonedDateTime'].includes(fieldType)) { _%>
             await <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('01/01/2001' + protractor.Key.TAB + '02:30AM');
             expect(await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input()).to.contain('2001-01-01T02:30');
+            <%_ } else if (fieldType === 'Duration') { _%>
+            await <%= entityInstance %>UpdatePage.set<%= fieldNameCapitalized %>Input('PT12S');
+            expect(await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input()).to.contain('12');
             <%_ } else if (fieldType === 'Boolean') { _%>
             const selected<%= fieldNameCapitalized %> = await <%= entityInstance %>UpdatePage.get<%= fieldNameCapitalized %>Input().isSelected();
             if (selected<%= fieldNameCapitalized %>) {
diff --git a/generators/entity-server/templates/src/test/java/package/web/rest/EntityResourceIT.java.ejs b/generators/entity-server/templates/src/test/java/package/web/rest/EntityResourceIT.java.ejs
index b7f2cb662b..1b82537e23 100644
--- a/generators/entity-server/templates/src/test/java/package/web/rest/EntityResourceIT.java.ejs
+++ b/generators/entity-server/templates/src/test/java/package/web/rest/EntityResourceIT.java.ejs
@@ -103,7 +103,7 @@ import org.springframework.validation.Validator;
 <% if (databaseType === 'sql') { %>
 import javax.persistence.EntityManager;<% } %><% if (fieldsContainBigDecimal === true) { %>
 import java.math.BigDecimal;<% } %><% if (fieldsContainBlob === true && databaseType === 'cassandra') { %>
-import java.nio.ByteBuffer;<% } %><% if (reactiveRepositories) { %>
+import java.nio.ByteBuffer;<% } %><% if (reactiveRepositories || fieldsContainDuration === true) { %>
 import java.time.Duration;<% } %><% if (fieldsContainLocalDate === true) { %>
 import java.time.LocalDate;<% } %><% if (fieldsContainInstant === true || fieldsContainZonedDateTime === true) { %>
 import java.time.Instant;<% } %><% if (fieldsContainZonedDateTime === true) { %>
@@ -285,6 +285,10 @@ _%>
 
     private static final ZonedDateTime <%=defaultValueName %> = ZonedDateTime.ofInstant(Instant.ofEpochMilli(0L), ZoneOffset.UTC);
     private static final ZonedDateTime <%=updatedValueName %> = ZonedDateTime.now(ZoneId.systemDefault()).withNano(0);
+    <%_ } else if (fieldType === 'Duration') { _%>
+
+    private static final Duration <%=defaultValueName %> = Duration.ofHours(6);
+    private static final Duration <%=updatedValueName %> = Duration.ofHours(12);
     <%_ } else if (fieldType === 'Boolean') { _%>
 
     private static final Boolean <%=defaultValueName %> = false;
diff --git a/test-integration/samples/.jhipster/BankAccount.json b/test-integration/samples/.jhipster/BankAccount.json
index 67e35b170d..bf556dbd1f 100644
--- a/test-integration/samples/.jhipster/BankAccount.json
+++ b/test-integration/samples/.jhipster/BankAccount.json
@@ -38,6 +38,10 @@
             "fieldName": "meanOperationDuration",
             "fieldType": "Double"
         },
+        {
+            "fieldName": "meanQueueDuration",
+            "fieldType": "Duration"
+        },
         {
             "fieldName": "balance",
             "fieldType": "BigDecimal",
diff --git a/test-integration/samples/.jhipster/FieldTestEntity.json b/test-integration/samples/.jhipster/FieldTestEntity.json
index cb0f648577..0635b10132 100644
--- a/test-integration/samples/.jhipster/FieldTestEntity.json
+++ b/test-integration/samples/.jhipster/FieldTestEntity.json
@@ -198,6 +198,17 @@
                 "required"
             ]
         },
+        {
+            "fieldName": "durationTom",
+            "fieldType": "Duration"
+        },
+        {
+            "fieldName": "durationRequiredTom",
+            "fieldType": "Duration",
+            "fieldValidateRules": [
+                "required"
+            ]
+        },
         {
             "fieldName": "booleanTom",
             "fieldType": "Boolean"