| /** |
| * 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; |
| import org.eclipse.osbp.runtime.common.historized.UUIDHist; |
| |
| 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 (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() && !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 rmvd : getRemoved()) { |
| E removedE = mappingContext.get(childMapper.createEntityHash(rmvd)); |
| if (removedE == null) { |
| IDTOService<D> service = (IDTOService<D>) DtoServiceAccess.getService(dtoType); |
| Object id = service.getId((D) rmvd); |
| removedE = (E) mappingContext.findEntityByEntityManager(childMapper.createEntity().getClass(), id); |
| if (removedE == null) { |
| removedE = childMapper.createEntity(); |
| childMapper.mapToEntity(rmvd, removedE, mappingContext); |
| } |
| } |
| oppositeRemover.accept(removedE); |
| super.remove(rmvd); |
| addedToSuper.remove(hashDto(rmvd)); |
| } |
| 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 |
| // |
| childMapper.mapToEntity(modified, modifiedE, mappingContext); |
| mappingContext.register(childMapper.createEntityHash(modified), modifiedE); |
| oppositeAdder.accept(modifiedE); |
| if (isResolved() && !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); |
| |
| added.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); |
| Query query = internalContainsInDatabase(id); |
| return service.contains(query); |
| } |
| |
| private Query internalContainsInDatabase(Object id) { |
| Query query = null; |
| if (DtoUtils.isHistorizedId(id)) { |
| UUIDHist histId = (UUIDHist) id; |
| Equal idEquals = new LCompare.Equal(DtoUtils.getIdField(dtoType).getName(), histId.id); |
| Equal validFromEquals = new LCompare.Equal(DtoUtils.getIdValidFromField(dtoType).getName(), |
| histId.validFrom); |
| Equal parentEquals = new LCompare.Equal(parentProperty, idSupplier.get()); |
| query = new Query(new LAnd(idEquals, validFromEquals, parentEquals)); |
| } else { |
| Equal idEquals = new LCompare.Equal(DtoUtils.getIdField(dtoType).getName(), id); |
| Equal parentEquals = new LCompare.Equal(parentProperty, idSupplier.get()); |
| query = new Query(new LAnd(idEquals, parentEquals)); |
| } |
| return 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 != null && !(o instanceof AbstractOppositeDtoList)) { |
| return o.equals(this); |
| } |
| |
| AbstractOppositeDtoList<D> other = (AbstractOppositeDtoList<D>) o; |
| if(other != null) { |
| 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(); |
| } |
| } |