From ab2a0e2301ada90676c90e435886ef44a7d57efb Mon Sep 17 00:00:00 2001 From: Joern Muehlencord Date: Fri, 21 Jun 2019 09:45:16 +0200 Subject: [PATCH] added EntityUtil, ensured cloned entities are updated to represent a new entity --- db/pom.xml | 23 ++ .../shared/db/AbstractController.java | 30 +-- .../shared/db/ControllerException.java | 125 +++++----- .../de/muehlencord/shared/db/EntityUtil.java | 116 ++++++++++ db/src/main/resources/log4j.xml | 32 +++ .../muehlencord/shared/db/CounterEntity.java | 94 ++++++++ .../muehlencord/shared/db/EntityUtilTest.java | 63 ++++++ .../de/muehlencord/shared/db/TestEntity.java | 214 ++++++++++++++++++ pom.xml | 22 +- 9 files changed, 633 insertions(+), 86 deletions(-) create mode 100644 db/src/main/java/de/muehlencord/shared/db/EntityUtil.java create mode 100644 db/src/main/resources/log4j.xml create mode 100644 db/src/test/java/de/muehlencord/shared/db/CounterEntity.java create mode 100644 db/src/test/java/de/muehlencord/shared/db/EntityUtilTest.java create mode 100644 db/src/test/java/de/muehlencord/shared/db/TestEntity.java diff --git a/db/pom.xml b/db/pom.xml index abf9a62..85a7903 100644 --- a/db/pom.xml +++ b/db/pom.xml @@ -23,10 +23,33 @@ ${project.groupId} shared-util + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.slf4j slf4j-api + + org.slf4j + slf4j-log4j12 + test + javax javaee-web-api diff --git a/db/src/main/java/de/muehlencord/shared/db/AbstractController.java b/db/src/main/java/de/muehlencord/shared/db/AbstractController.java index 15a609e..b6baec5 100644 --- a/db/src/main/java/de/muehlencord/shared/db/AbstractController.java +++ b/db/src/main/java/de/muehlencord/shared/db/AbstractController.java @@ -41,7 +41,6 @@ import javax.persistence.metamodel.IdentifiableType; import javax.persistence.metamodel.Metamodel; import javax.persistence.metamodel.SingularAttribute; import javax.transaction.Transactional; -import org.apache.commons.lang3.SerializationUtils; import org.apache.commons.lang3.StringUtils; /** @@ -91,8 +90,9 @@ public abstract class AbstractController { * @param cb the criteria builder to use * @param root the root of the object to search for * @param filters the filters to apply - * @param include if set to true, the filter is used as include filter (equals, in). If set to false, the filter is - * inverted and used as exclude filter (not equals, not in etc) + * @param include if set to true, the filter is used as include filter + * (equals, in). If set to false, the filter is inverted and used as exclude + * filter (not equals, not in etc) * @return */ protected Predicate getFilterCondition(Predicate filterCondition, CriteriaBuilder cb, Root root, Map filters, boolean include) { @@ -231,7 +231,7 @@ public abstract class AbstractController { @Lock(LockType.WRITE) public T update(T entity, String updatedBy) throws ControllerException { T currentEntity = attach(entity); - T newEntity = getClone(currentEntity); + T newEntity = EntityUtil.cloneToNewEntity(currentEntity); if (Auditable.class.isAssignableFrom(entity.getClass())) { Audit audit = ((Auditable) entity).getAudit(); ((Auditable) entity).setAudit(applyAuditChanges(audit, false, updatedBy)); @@ -243,7 +243,7 @@ public abstract class AbstractController { endDateable.setValidTo(DateUtil.getCurrentTimeInUTC()); em.merge(entity); // and create new entity instead - + return create(newEntity, updatedBy); } else { // if it is not enddatable, just update it (already done above) @@ -325,8 +325,9 @@ public abstract class AbstractController { } /** - * returns null, if the list is empty or null itself. Returns the one element if there is exactly one element in the - * list. Otherwise an exception is thrown + * returns null, if the list is empty or null itself. Returns the one + * element if there is exactly one element in the list. Otherwise an + * exception is thrown * * @param resultList * @return @@ -348,19 +349,4 @@ public abstract class AbstractController { // of.getDeclaredId(entityClass).getJavaMember(). return of.getId(of.getIdType().getJavaType()); } - - private T getClone(T entity) { - T newEntity = SerializationUtils.clone(entity); - // remove audit if class is auditable - it is a new clone - if (Auditable.class.isAssignableFrom(newEntity.getClass())) { - ((Auditable) newEntity).setAudit(null); - } - // set new valid dates if class is enddateable - if (EndDateable.class.isAssignableFrom(newEntity.getClass())) { - ((EndDateable) newEntity).setValidFrom(DateUtil.getCurrentTimeInUTC()); - ((EndDateable) newEntity).setValidTo(null); - } - return newEntity; - } - } diff --git a/db/src/main/java/de/muehlencord/shared/db/ControllerException.java b/db/src/main/java/de/muehlencord/shared/db/ControllerException.java index a0d022f..978472a 100644 --- a/db/src/main/java/de/muehlencord/shared/db/ControllerException.java +++ b/db/src/main/java/de/muehlencord/shared/db/ControllerException.java @@ -1,62 +1,63 @@ -/* - * Copyright 2019 joern.muehlencord. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package de.muehlencord.shared.db; - -import javax.ejb.ApplicationException; - -/** - * - * @author joern.muehlencord - */ -@ApplicationException(rollback=true) -public class ControllerException extends Exception { - - private static final long serialVersionUID = 5190280225284514859L; - public static final int CAUSE_ALREADY_EXISTS = 1; - public static final int CAUSE_NOT_FOUND = 2; - public static final int CAUSE_CANNOT_PERSIST = 3; - public static final int CAUSE_TOO_MANY_ROWS = 4; - public static final int CAUSE_CANNOT_DELETE = 5; - - private final int causeCode; - - /** - * Creates a new instance of ControllerException without detail - * message. - * - * @param cause the reason code - * @param message an explanation - */ - public ControllerException(int cause, String message) { - super(message); - this.causeCode = cause; - } - - /** - * - * @param causeCode - * @param message - * @param cause - */ - public ControllerException(int causeCode, String message, Throwable cause) { - super(message, cause); - this.causeCode = causeCode; - } - - public int getCauseCode() { - return causeCode; - } -} +/* + * Copyright 2019 joern.muehlencord. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.db; + +import javax.ejb.ApplicationException; + +/** + * + * @author joern.muehlencord + */ +@ApplicationException(rollback = true) +public class ControllerException extends Exception { + + private static final long serialVersionUID = 5190280225284514859L; + public static final int INTERNAL_ERROR = 0; + public static final int CAUSE_ALREADY_EXISTS = 1; + public static final int CAUSE_NOT_FOUND = 2; + public static final int CAUSE_CANNOT_PERSIST = 3; + public static final int CAUSE_TOO_MANY_ROWS = 4; + public static final int CAUSE_CANNOT_DELETE = 5; + + private final int causeCode; + + /** + * Creates a new instance of ControllerException without detail + * message. + * + * @param cause the reason code + * @param message an explanation + */ + public ControllerException(int cause, String message) { + super(message); + this.causeCode = cause; + } + + /** + * + * @param causeCode + * @param message + * @param cause + */ + public ControllerException(int causeCode, String message, Throwable cause) { + super(message, cause); + this.causeCode = causeCode; + } + + public int getCauseCode() { + return causeCode; + } +} diff --git a/db/src/main/java/de/muehlencord/shared/db/EntityUtil.java b/db/src/main/java/de/muehlencord/shared/db/EntityUtil.java new file mode 100644 index 0000000..4fa5267 --- /dev/null +++ b/db/src/main/java/de/muehlencord/shared/db/EntityUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright 2019 joern.muehlencord. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.db; + +import de.muehlencord.shared.util.DateUtil; +import java.beans.IntrospectionException; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import javax.persistence.Id; +import org.apache.commons.lang3.SerializationUtils; +import org.slf4j.LoggerFactory; + +/** + * + * @author joern.muehlencord + */ +public class EntityUtil { + + private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(EntityUtil.class); + + public static Field getIdField(Class entity) { + if (entity == null) { + return null; + } + + for (var f : entity.getDeclaredFields()) { + Id id = null; + Annotation[] as = f.getAnnotations(); + for (Annotation a : as) { + if (a.annotationType() == Id.class) { + id = (Id) a; + } + } + + if (id != null) { + return f; + } + } + + // iterated over all fields, not found + return null; + } + + public static T setIdValue(T entity, Object fieldValue) throws ControllerException { + Field field = getIdField(entity.getClass()); + + if (field == null) { + return entity; + } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("id column of {} is {}", entity.getClass().getSimpleName(), field.getName()); + } + + try { + PropertyDescriptor pd = new PropertyDescriptor(field.getName(), entity.getClass()); + pd.getWriteMethod().invoke(entity, fieldValue); + return entity; + } catch (IllegalAccessException | InvocationTargetException | IntrospectionException ex) { + String hint = "Error setting value of field " + field.getName() + " to " + fieldValue; + if (fieldValue != null) { + hint += " of type " + fieldValue.getClass().getSimpleName(); + } + throw new ControllerException(ControllerException.INTERNAL_ERROR, hint, ex); + } + + } + + /** + * clones the given entity and updates related fields so the entity appears as new.The following changes are executed + *
    + *
  • the Id field of the entity is set to null
  • + *
  • if the entity is auditable, the audit is set to null.
  • + *
  • if the entity is enddatable, validTo is set to null and validFrom is set to current sysdate (in UTC) + *
+ * + * @param the entity to be cloned - must implement serializeable to be cloned + * @param entity the entity to be cloned - must implement serializeable to be cloned + * @return the cloned entity with updated fields as described above + * @throws de.muehlencord.shared.db.ControllerException if the id value cannot be set to null + */ + public static T cloneToNewEntity(T entity) throws ControllerException { + T newEntity = SerializationUtils.clone(entity); + + // ensure id column is set to null + newEntity = setIdValue(newEntity, null); + + // remove audit if class is auditable - it is a new clone + if (Auditable.class.isAssignableFrom(newEntity.getClass())) { + ((Auditable) newEntity).setAudit(null); + } + + // set new valid dates if class is enddateable + if (EndDateable.class.isAssignableFrom(newEntity.getClass())) { + ((EndDateable) newEntity).setValidFrom(DateUtil.getCurrentTimeInUTC()); + ((EndDateable) newEntity).setValidTo(null); + } + return newEntity; + } + +} diff --git a/db/src/main/resources/log4j.xml b/db/src/main/resources/log4j.xml new file mode 100644 index 0000000..4388d5e --- /dev/null +++ b/db/src/main/resources/log4j.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/db/src/test/java/de/muehlencord/shared/db/CounterEntity.java b/db/src/test/java/de/muehlencord/shared/db/CounterEntity.java new file mode 100644 index 0000000..4729025 --- /dev/null +++ b/db/src/test/java/de/muehlencord/shared/db/CounterEntity.java @@ -0,0 +1,94 @@ +package de.muehlencord.shared.db; + +import java.io.Serializable; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * + * @author joern.muehlencord + */ +@Entity +@Table(name = "counter") +@NamedQueries({ + @NamedQuery(name = "CounterEntity.findAll", query = "SELECT c FROM CounterEntity c") + , @NamedQuery(name = "CounterEntity.findByCounterKey", query = "SELECT c FROM CounterEntity c WHERE c.counterKey = :counterKey") + , @NamedQuery(name = "CounterEntity.findByCounterDefinition", query = "SELECT c FROM CounterEntity c WHERE c.counterDefinition = :counterDefinition")}) +public class CounterEntity implements Serializable { + + private static final long serialVersionUID = -5104103828013760003L; + + + @Id + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 100) + @Column(name = "counter_key") + private String counterKey; + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 100) + @Column(name = "counter_definition") + private String counterDefinition; + + public CounterEntity() { + } + + public CounterEntity(String counterKey) { + this.counterKey = counterKey; + } + + public CounterEntity(String counterKey, String counterDefinition) { + this.counterKey = counterKey; + this.counterDefinition = counterDefinition; + } + + public String getCounterKey() { + return counterKey; + } + + public void setCounterKey(String counterKey) { + this.counterKey = counterKey; + } + + public String getCounterDefinition() { + return counterDefinition; + } + + public void setCounterDefinition(String counterDefinition) { + this.counterDefinition = counterDefinition; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (counterKey != null ? counterKey.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof CounterEntity)) { + return false; + } + CounterEntity other = (CounterEntity) object; + if ((this.counterKey == null && other.counterKey != null) || (this.counterKey != null && !this.counterKey.equals(other.counterKey))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "de.muehlencord.office.entity.core.CounterEntity[ counterKey=" + counterKey + " ]"; + } + +} diff --git a/db/src/test/java/de/muehlencord/shared/db/EntityUtilTest.java b/db/src/test/java/de/muehlencord/shared/db/EntityUtilTest.java new file mode 100644 index 0000000..5830521 --- /dev/null +++ b/db/src/test/java/de/muehlencord/shared/db/EntityUtilTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 joern.muehlencord. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package de.muehlencord.shared.db; + +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author joern.muehlencord + */ +public class EntityUtilTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(EntityUtilTest.class); + + @Test + public void testGetId() { + Field idField = EntityUtil.getIdField(CounterEntity.class); + assertNotNull(idField); + assertEquals("counterKey", idField.getName()); + } + + @Test + public void testSetIdNull() throws IllegalArgumentException, IllegalAccessException, ControllerException { + CounterEntity counter = new CounterEntity(); + counter.setCounterDefinition("counterDefinitionValue"); + counter.setCounterKey("counterKeyValue"); + LOGGER.info("Counter after creation: {}", counter.toString()); + + Field idField = EntityUtil.getIdField(CounterEntity.class); + + assertNotNull(idField); + + assertEquals("counterKey", idField.getName()); +// assertEquals("counterKeyValue", idField.get(counter)); + counter = EntityUtil.setIdValue(counter, null); +// assertNull (idField.get (counter)); + assertNull (counter.getCounterKey()); + + LOGGER.info("Counter after update: {}", counter.toString()); + + + } + +} diff --git a/db/src/test/java/de/muehlencord/shared/db/TestEntity.java b/db/src/test/java/de/muehlencord/shared/db/TestEntity.java new file mode 100644 index 0000000..64c6ab4 --- /dev/null +++ b/db/src/test/java/de/muehlencord/shared/db/TestEntity.java @@ -0,0 +1,214 @@ +package de.muehlencord.shared.db; + +import java.io.Serializable; +import java.util.Date; +import java.util.UUID; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * + * @author joern.muehlencord + */ +@Entity +@Table(name = "address") +@NamedQueries({ + @NamedQuery(name = "AddressEntity.findAll", query = "SELECT a FROM AddressEntity a"), + @NamedQuery(name = "AddressEntity.findByStreet", query = "SELECT a FROM AddressEntity a WHERE a.street = :street"), + @NamedQuery(name = "AddressEntity.findByPostalArea", query = "SELECT a FROM AddressEntity a WHERE a.postalArea = :postalArea"), + @NamedQuery(name = "AddressEntity.findByCity", query = "SELECT a FROM AddressEntity a WHERE a.city = :city"), + @NamedQuery(name = "AddressEntity.findByValidFrom", query = "SELECT a FROM AddressEntity a WHERE a.validFrom = :validFrom"), + @NamedQuery(name = "AddressEntity.findByValidTo", query = "SELECT a FROM AddressEntity a WHERE a.validTo = :validTo")}) +public class TestEntity implements Serializable, Auditable, EndDateable { + + private static final long serialVersionUID = 1L; + @Id + @Basic(optional = false) + @NotNull + @Column(name = "id") + @GeneratedValue(generator = "uuid2") + private UUID id; + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 200) + @Column(name = "street") + private String street; + @Size(max = 10) + @Column(name = "street_number") + private String streetNumber; + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 10) + @Column(name = "postal_area") + private String postalArea; + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 50) + @Column(name = "city") + private String city; + @Column(name = "government_district") + private String governmentDistrict; + @Min(value = -90) + @Max(value = 90) + @Column(name = "latitude") + private Double latitude; + @Min(value = -180) + @Max(value = 180) + @Column(name = "longitude") + private Double longitude; + @Column(name = "valid_from") + @Temporal(TemporalType.TIMESTAMP) + private Date validFrom; + @Column(name = "valid_to") + @Temporal(TemporalType.TIMESTAMP) + private Date validTo; + @Embedded + private Audit audit; + + public TestEntity() { + // empty constructor required for JPA + } + + public UUID getId() { + return id; + } + + public String getIdString() { + if (id == null) { + return "unknown"; + } else { + return id.toString(); + } + } + + public void setId(UUID id) { + this.id = id; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getStreetNumber() { + return streetNumber; + } + + public void setStreetNumber(String streetNumber) { + this.streetNumber = streetNumber; + } + + public String getPostalArea() { + return postalArea; + } + + public void setPostalArea(String postalArea) { + this.postalArea = postalArea; + } + + public String getGovernmentDistrict() { + return governmentDistrict; + } + + public void setGovernmentDistrict(String governmentDistrict) { + this.governmentDistrict = governmentDistrict; + } + + public Double getLatitude() { + return latitude; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public Double getLongitude() { + return longitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + @Override + public Date getValidFrom() { + return validFrom; + } + + @Override + public void setValidFrom(Date validFrom) { + this.validFrom = validFrom; + } + + @Override + public Date getValidTo() { + return validTo; + } + + @Override + public void setValidTo(Date validTo) { + this.validTo = validTo; + } + + @Override + public Audit getAudit() { + return audit; + } + + @Override + public void setAudit(Audit audit) { + this.audit = audit; + } + + + @Override + public int hashCode() { + int hash = 0; + hash += (id != null ? id.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof TestEntity)) { + return false; + } + TestEntity other = (TestEntity) object; + if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "de.muehlencord.office.entity.party.AddressEntity[ id=" + id + " ]"; + } + + +} diff --git a/pom.xml b/pom.xml index 500a627..f60d9ea 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,25 @@ junit 4.12 test -
+ + + org.junit.jupiter + junit-jupiter-api + 5.3.1 + test + + + org.junit.jupiter + junit-jupiter-params + 5.3.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.3.1 + test + org.mockito mockito-core @@ -108,7 +126,7 @@ org.slf4j slf4j-api - 1.7.25 + 1.7.26 org.slf4j