blob: 2ac8eb3a0c69ebb433319beeb54d61083d75bec2 [file] [log] [blame]
/**
* Copyright (c) 2011, 2015 - Lunifera GmbH (Gross Enzersdorf, Austria), Loetz GmbH&Co.KG (69115 Heidelberg, Germany)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Florian Pirchner - Initial implementation
*/
package org.eclipse.osbp.dsl.dto.lib;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import org.eclipse.osbp.dsl.common.datatypes.IDto;
import org.eclipse.osbp.dsl.dto.lib.impl.DtoServiceAccess;
import org.eclipse.osbp.jpa.services.Query;
import org.eclipse.osbp.jpa.services.filters.LAnd;
import org.eclipse.osbp.jpa.services.filters.LCompare;
import org.eclipse.osbp.jpa.services.filters.LCompare.Equal;
import org.eclipse.osbp.runtime.common.annotations.DtoUtils;
import org.eclipse.osbp.runtime.common.filter.IDTOService;
import org.eclipse.osbp.runtime.common.hash.HashUtil;
public abstract class AbstractOppositeDtoList<D extends IDto> extends ArrayList<D>
implements IEntityMappingList<D>, PropertyChangeListener, Serializable {
protected static final String NOT_SUPPORTED_FOR_NOW = "Not supported for now.";
private static final long serialVersionUID = -5777389952283179658L;
protected final Class<?> dtoType;
protected final String parentProperty;
protected final transient Supplier<Object> idSupplier;
protected final transient PropertyChangeListener childListener;
protected boolean immutableCopy;
protected boolean resolved;
protected boolean resolving;
protected transient Map<Object, D> addedToSuper = new HashMap<>();
protected transient Map<Object, D> added = new HashMap<>();
protected transient Map<Object, D> removed = new HashMap<>();
protected transient Map<Object, D> updated = new HashMap<>();
protected transient Map<Object, D> persistent = new HashMap<>();
protected MappingContext mappingContext;
protected transient PropertyChangeListener containerListener;
public AbstractOppositeDtoList(MappingContext mappingContext, Class<?> dtoType, String parentProperty,
Supplier<Object> idSupplier, PropertyChangeListener containerListener) {
super();
this.mappingContext = mappingContext != null ? mappingContext : new MappingContext();
this.dtoType = dtoType;
this.idSupplier = idSupplier;
this.parentProperty = parentProperty;
this.containerListener = containerListener;
childListener = (e) -> {
containerListener.propertyChange(e);
};
}
public List<D> getAdded() {
return new ArrayList<>(added.values());
}
public List<D> getRemoved() {
return new ArrayList<>(removed.values());
}
public List<D> getUpdated() {
return new ArrayList<>(updated.values());
}
public List<D> getAddedToSuper() {
return new ArrayList<>(addedToSuper.values());
}
/**
* Notifies the container of this list in case of cascading reference.
*
* @param changedDto
*/
protected void notifyContainer(D changedDto) {
// implement in subclass
}
@SuppressWarnings("unchecked")
protected void resolve() {
syncContextWithTransaction();
if (resolved || resolving || mappingContext.isNoCollectionResolving()) {
return;
}
boolean oldDirtyStateActive = mappingContext.isDirtyAdapterActive();
try {
resolving = true;
mappingContext.makeCurrent();
mappingContext.setDirtyAdapterActive(false);
IDTOService<?> service = DtoServiceAccess.getService(dtoType);
Query query = new Query(new LCompare.Equal(parentProperty, idSupplier.get()));
Collection<D> dtos = (Collection<D>) service.find(query);
// remove the removed from the added
for (D rmvd : getRemoved()) {
String hash = hashDto(rmvd);
added.remove(hash);
// remove from super
D toSuper = addedToSuper.remove(hash);
if (toSuper != null) {
super.remove(toSuper);
}
}
// now merge the query result
if (dtos.isEmpty()) {
// if result is empty, add all added
super.addAll(getAdded());
addedToSuper.putAll(added);
} else {
HashMap<Object, D> tempAdded = new HashMap<>(added);
for (D dto : dtos) {
String hashCode = hashDto(dto);
if (removed.containsKey(hashCode)) {
// if it was removed, we do not add the dto
continue;
} else {
if (tempAdded.containsKey(hashCode)) {
// if it was added and contained in the DB, then we
// add the added one
D instance = tempAdded.get(hashCode);
super.add(instance);
addedToSuper.put(hashCode, instance);
persistent.put(hashCode, instance);
tempAdded.remove(hashCode);
} else {
// otherwise we add the persistent dto
super.add(dto);
addedToSuper.put(hashCode, dto);
persistent.put(hashCode, dto);
// do not add listeners for immutable copies
if (!immutableCopy) {
// register property change listener to the
// persisted dto
if (DtoUtils.getAdapter(getClass(), dto) == null) {
DtoUtils.registerAdapter(this, dto);
}
dto.addPropertyChangeListener(childListener);
}
}
}
}
// now we add the pending added
super.addAll(tempAdded.values());
addedToSuper.putAll(tempAdded);
}
} catch (Exception ex) {
resolved = false;
resolving = false;
throw new RuntimeException(ex);
} finally {
resolved = true;
resolving = false;
mappingContext.setDirtyAdapterActive(oldDirtyStateActive);
mappingContext.unmakeCurrent();
}
}
public boolean isPersistent(D dto) {
String hash = hashDto(dto);
if (resolved) {
return persistent.containsKey(hash);
} else {
return containsInDatabase(dto);
}
}
/*
* (non-Javadoc)
*
* @see
* org.eclipse.osbp.dsl.dto.lib.IEntityMappingList#mapToEntity(org.eclipse
* .osbp.dsl.dto.lib.IMapper, java.util.function.Consumer,
* java.util.function.Consumer)
*/
@SuppressWarnings("unchecked")
@Override
public <E> void mapToEntity(IMapper<D, E> childMapper, Consumer<E> oppositeAdder, Consumer<E> oppositeRemover) {
syncContextWithTransaction();
boolean oldNoContainerMapping = mappingContext.isNoContainerMapping();
try {
mappingContext.makeCurrent();
// no container mapping! Otherwise childMapper.mapToEntity will
// map the opposite side, which will cause a wrong container entity
mappingContext.setNoContainerMapping(true);
for (D add : getAdded()) {
E addedE = mappingContext.get(childMapper.createEntityHash(add));
if (addedE == null) {
addedE = childMapper.createEntity();
childMapper.mapToEntity(add, addedE, mappingContext);
}
oppositeAdder.accept(addedE);
if (isResolved()) {
if (!super.contains(add)) {
super.add(add);
addedToSuper.put(hashDto(add), add);
}
}
}
added.clear();
// for remove we use container mapping! The container is null
// and removeFromChildren will not remove the elements.
mappingContext.setNoContainerMapping(true);
for (D removed : getRemoved()) {
E removedE = mappingContext.get(childMapper.createEntityHash(removed));
if (removedE == null) {
IDTOService<D> service = (IDTOService<D>) DtoServiceAccess.getService(dtoType);
Object id = service.getId((D) removed);
removedE = (E) mappingContext.findEntityByEntityManager(childMapper.createEntity().getClass(), id);
if (removedE == null) {
removedE = childMapper.createEntity();
childMapper.mapToEntity(removed, removedE, mappingContext);
}
}
oppositeRemover.accept(removedE);
super.remove(removed);
addedToSuper.remove(hashDto(removed));
}
removed.clear();
// handle the changed values by remove and add
//
for (D modified : getUpdated()) {
// remove
//
E modifiedE = mappingContext.get(childMapper.createEntityHash(modified));
if (modifiedE == null) {
IDTOService<D> service = (IDTOService<D>) DtoServiceAccess.getService(dtoType);
Object id = service.getId((D) modified);
modifiedE = (E) mappingContext.findEntityByEntityManager(childMapper.createEntity().getClass(), id);
if (modifiedE == null) {
modifiedE = childMapper.createEntity();
childMapper.mapToEntity(modified, modifiedE, mappingContext);
}
}
oppositeRemover.accept(modifiedE);
if (isResolved()) {
super.remove(modified);
addedToSuper.remove(hashDto(modified));
}
// add
//
// modifiedE = childMapper.createEntity();
childMapper.mapToEntity(modified, modifiedE, mappingContext);
mappingContext.register(childMapper.createEntityHash(modified), modifiedE);
oppositeAdder.accept(modifiedE);
if (isResolved()) {
if (!super.contains(modified)) {
super.add(modified);
addedToSuper.put(hashDto(modified), modified);
}
}
}
updated.clear();
} finally {
mappingContext.setNoContainerMapping(oldNoContainerMapping);
mappingContext.unmakeCurrent();
}
}
@SuppressWarnings("unchecked")
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (removed.containsKey(hashDto((D) evt.getSource()))) {
return;
}
if (!Objects.equals(evt.getOldValue(), evt.getNewValue())) {
updated.put(hashDto((D) evt.getSource()), (D) evt.getSource());
// notify the container that detail changed
notifyContainer((D) evt.getSource());
}
}
/**
* Checks if there is a current context from a transaction done right now.
* If so then use this one as the new mapping context since it is connected
* to the root service.
*/
private void syncContextWithTransaction() {
MappingContext currentContext = MappingContext.getCurrent();
if (currentContext != null) {
mappingContext = currentContext;
}
}
public boolean isResolved() {
return resolved;
}
private String hashDto(D dto) {
return HashUtil.createObjectWithIdHash(dto.getClass(), dto);
}
@Override
public Stream<D> parallelStream() {
resolve();
return super.parallelStream();
}
@Override
public Stream<D> stream() {
resolve();
return super.stream();
}
@Override
public boolean add(D dto) {
checkImmutable();
dto.addPropertyChangeListener(childListener);
String hash = hashDto(dto);
removed.remove(hash);
// if (!isPersistent(dto)) {
// we do not add persistent dtos twice. They are already in the
// database.
added.put(hash, dto);
// } else {
// // rather we put them in updated state
// updated.put(hash, dto);
// }
// notify the container that detail changed
notifyContainer(dto);
if (resolving) {
return false;
}
if (!resolved) {
return true;
} else {
// add to super
addedToSuper.put(hash, dto);
return super.add(dto);
}
}
protected void checkImmutable() {
if (immutableCopy) {
throw new IllegalStateException("No modifications allowed for immutableCopy list.");
}
}
@Override
public void add(int arg0, D dto) {
add(dto);
}
@Override
public boolean addAll(Collection<? extends D> dtos) {
boolean result = true;
for (D dto : dtos) {
result &= add(dto);
}
return result;
}
/**
* Used for copy list
*
* @param dtos
*/
protected void internalAddAll(Collection<? extends D> dtos) {
super.addAll(dtos);
}
@Override
public boolean addAll(int arg0, Collection<? extends D> dtos) {
boolean result = true;
for (D dto : dtos) {
result &= add(dto);
}
return result;
}
@Override
public void clear() {
super.clear();
added.clear();
removed.clear();
}
protected abstract AbstractOppositeDtoList<D> newInstance();
public AbstractOppositeDtoList<D> copy() {
AbstractOppositeDtoList<D> clone = newInstance();
clone.resolved = resolved;
clone.immutableCopy = true;
// only add super#list to the clone if resolved. Otherwise this list
// will resolve.
if (resolved) {
clone.internalAddAll(this);
}
clone.added.putAll(added);
clone.removed.putAll(removed);
clone.updated.putAll(updated);
clone.addedToSuper.putAll(addedToSuper);
clone.mappingContext = mappingContext;
return clone;
}
@SuppressWarnings("unchecked")
@Override
public boolean contains(Object dto) {
String hash = hashDto((D) dto);
if (added.containsKey(hash)) {
return true;
}
if (removed.containsKey(hash)) {
return false;
}
if (isResolved()) {
return addedToSuper.containsKey(hash);
} else {
return containsInDatabase(dto);
}
}
@SuppressWarnings("unchecked")
protected boolean containsInDatabase(Object dto) {
IDTOService<D> service = (IDTOService<D>) DtoServiceAccess.getService(dtoType);
Object id = service.getId((D) dto);
Equal idEquals = new LCompare.Equal(DtoUtils.getIdField(dtoType).getName(), id);
Equal parentEquals = new LCompare.Equal(parentProperty, idSupplier.get());
Query query = new Query(new LAnd(idEquals, parentEquals));
return service.contains(query);
}
@Override
public void forEach(Consumer<? super D> arg0) {
resolve();
super.forEach(arg0);
}
@Override
public D get(int arg0) {
resolve();
return super.get(arg0);
}
@Override
public int indexOf(Object arg0) {
resolve();
return super.indexOf(arg0);
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public Iterator<D> iterator() {
resolve();
return super.iterator();
}
@Override
public int lastIndexOf(Object arg0) {
resolve();
return super.lastIndexOf(arg0);
}
@Override
public ListIterator<D> listIterator() {
resolve();
return super.listIterator();
}
@Override
public ListIterator<D> listIterator(int arg0) {
resolve();
return super.listIterator(arg0);
}
@Override
public D remove(int arg0) {
checkImmutable();
// now we need to resolve, since we got an index...
resolve();
D result = super.remove(arg0);
addedToSuper.remove(hashDto(result));
updated.remove(hashDto(result));
removed.put(hashDto(result), result);
DtoUtils.unregisterAdapter(this, result);
result.removePropertyChangeListener(childListener);
// notify the container that detail changed
notifyContainer(result);
return result;
}
@SuppressWarnings("unchecked")
@Override
public boolean remove(Object dto) {
checkImmutable();
String hash = hashDto((D) dto);
if (!resolved) {
// we can not remove persistent dtos since they have not been loaded
// yet. So only check the removed, added and updated
added.remove(hash);
updated.remove(hash);
} else {
if (added.containsKey(hash)) {
// in this case we added a new dto, so just remove it from the
// added map again. We do not need to store the dto in the
// removed map for later processing. Dto is a transient dto and
// not stored in DB yet.
added.remove(hash);
updated.remove(hash);
} else if (addedToSuper.containsKey(hash)) {
// the removed element is part of the persistent elements. So we
// need to store the removed element in the removed map. For
// later processing.
removed.put(hash, (D) dto);
updated.remove(hash);
}
}
DtoUtils.unregisterAdapter(this, dto);
((IDto) dto).removePropertyChangeListener(childListener);
// notify the container that detail changed
notifyContainer((D) dto);
if (!resolved) {
return true;
} else {
D ref = addedToSuper.remove(hash);
return super.remove(ref);
}
}
@SuppressWarnings("unchecked")
@Override
public boolean removeAll(Collection<?> arg0) {
checkImmutable();
boolean result = true;
for (Object x : arg0) {
D dto = (D) x;
result &= remove(dto);
}
return result;
}
@Override
public boolean removeIf(Predicate<? super D> arg0) {
throw new UnsupportedOperationException(NOT_SUPPORTED_FOR_NOW);
}
@Override
protected void removeRange(int arg0, int arg1) {
throw new UnsupportedOperationException(NOT_SUPPORTED_FOR_NOW);
}
@Override
public void replaceAll(UnaryOperator<D> arg0) {
throw new UnsupportedOperationException(NOT_SUPPORTED_FOR_NOW);
}
@Override
public boolean retainAll(Collection<?> arg0) {
throw new UnsupportedOperationException(NOT_SUPPORTED_FOR_NOW);
}
@Override
public D set(int arg0, D arg1) {
checkImmutable();
resolve();
D dto = super.set(arg0, arg1);
removed.put(hashDto(dto), dto);
added.put(hashDto(arg1), arg1);
return dto;
}
@Override
public int size() {
if (resolved) {
return super.size();
} else if (mappingContext.isNoCollectionResolving()) {
return 0;
}
IDTOService<?> service = DtoServiceAccess.getService(dtoType);
checkSanity();
// in case of NOT resolved, we can use a query to calculate the size.
// size in DB + addedDtos - removedDtos --> In normal case,
Query query = new Query(new LCompare.Equal(parentProperty, idSupplier.get()));
return service.size(query) + getAdded().size() - getRemoved().size();
}
/**
* Checks whether this list is in a bad state.
*
* @throws IllegalArgumentException
*/
protected void checkSanity() throws IllegalArgumentException {
if (!resolved) {
if (!getRemoved().isEmpty()) {
throw new IllegalArgumentException(
"Non resolved lists may not contain removed elements. These elements can only by transient dtos. So elements needed to be added first. But removing transient elements from the added map will not store them in the removed map.");
}
}
}
@Override
public void sort(Comparator<? super D> arg0) {
checkImmutable();
resolve();
super.sort(arg0);
}
@Override
public Spliterator<D> spliterator() {
resolve();
return super.spliterator();
}
@Override
public List<D> subList(int arg0, int arg1) {
resolve();
return super.subList(arg0, arg1);
}
@Override
public Object[] toArray() {
resolve();
return super.toArray();
}
@Override
public <T> T[] toArray(T[] arg0) {
resolve();
return super.toArray(arg0);
}
@Override
public void trimToSize() {
checkImmutable();
resolve();
super.trimToSize();
}
@Override
public boolean containsAll(Collection<?> arg0) {
for (Object dto : arg0) {
if (!contains(dto)) {
return false;
}
}
return true;
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object o) {
if (!(o instanceof AbstractOppositeDtoList)) {
return o.equals(this);
}
AbstractOppositeDtoList<D> other = (AbstractOppositeDtoList<D>) o;
if (other.added.size() != added.size()) {
return false;
}
if (other.removed.size() != removed.size()) {
return false;
}
if (other.updated.size() != updated.size()) {
return false;
}
boolean equals = other.added.equals(added);
if (!equals) {
return false;
}
equals = other.removed.equals(removed);
if (!equals) {
return false;
}
equals = other.updated.equals(updated);
if (!equals) {
return false;
}
if (other.resolved && resolved) {
return super.equals(o);
}
return true;
}
@Override
public int hashCode() {
return super.hashCode();
}
}