added EntityUtil, ensured cloned entities are updated to represent a new entity

This commit is contained in:
Joern Muehlencord
2019-06-21 09:45:16 +02:00
parent a8e0f9bd5f
commit ab2a0e2301
9 changed files with 633 additions and 86 deletions

View File

@ -23,10 +23,33 @@
<groupId>${project.groupId}</groupId>
<artifactId>shared-util</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>

View File

@ -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<T extends Serializable> {
* @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<T> root, Map<String, Object> filters, boolean include) {
@ -231,7 +231,7 @@ public abstract class AbstractController<T extends Serializable> {
@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));
@ -325,8 +325,9 @@ public abstract class AbstractController<T extends Serializable> {
}
/**
* 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<T extends Serializable> {
// 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;
}
}

View File

@ -25,6 +25,7 @@ import javax.ejb.ApplicationException;
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;

View File

@ -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> 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
* <ul>
* <li>the Id field of the entity is set to null</li>
* <li>if the entity is auditable, the audit is set to null.</li>
* <li>if the entity is enddatable, validTo is set to null and validFrom is set to current sysdate (in UTC)
* </ul>
*
* @param <T> 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 extends Serializable> 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;
}
}

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/"
debug="false">
<appender name="consoleAppender" class="org.apache.log4j.ConsoleAppender">
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{ISO8601} %-5p [%c] %m%n" />
</layout>
</appender>
<category name="de.muehlencord">
<priority value="DEBUG"/>
</category>
<category name="com.sun">
<priority value="WARN"/>
</category>
<category name="javax.xml">
<priority value="WARN"/>
</category>
<logger name="org.hibernate">
<level value="info"/>
</logger>
<root>
<level value="DEBUG" />
<appender-ref ref="consoleAppender" />
</root>
</log4j:configuration>

View File

@ -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 + " ]";
}
}

View File

@ -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());
}
}

View File

@ -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<TestEntity> {
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 + " ]";
}
}

20
pom.xml
View File

@ -79,6 +79,24 @@
<version>4.12</version> <!-- TODO needs update to v5 -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@ -108,7 +126,7 @@
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
<version>1.7.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>