/*
* Copyright (C) 2015 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.jpa.sync.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.jpa.sync.client.shared.ConflictResponse;
import org.jboss.errai.jpa.sync.client.shared.DataSyncService;
import org.jboss.errai.jpa.sync.client.shared.DeleteResponse;
import org.jboss.errai.jpa.sync.client.shared.EntityComparator;
import org.jboss.errai.jpa.sync.client.shared.IdChangeResponse;
import org.jboss.errai.jpa.sync.client.shared.JpaAttributeAccessor;
import org.jboss.errai.jpa.sync.client.shared.NewRemoteEntityResponse;
import org.jboss.errai.jpa.sync.client.shared.SyncRequestOperation;
import org.jboss.errai.jpa.sync.client.shared.SyncResponse;
import org.jboss.errai.jpa.sync.client.shared.SyncableDataSet;
import org.jboss.errai.jpa.sync.client.shared.UpdateResponse;
public class DataSyncServiceImpl implements DataSyncService {
private final EntityManager em;
private final JpaAttributeAccessor attributeAccessor;
private final EntityComparator entityComparator;
public DataSyncServiceImpl(EntityManager em, JpaAttributeAccessor attributeAccessor) {
this.em = Assert.notNull(em);
this.attributeAccessor = Assert.notNull(attributeAccessor);
this.entityComparator = new EntityComparator(em.getMetamodel(), attributeAccessor);
}
@Override
public <E> List<SyncResponse<E>> coldSync(SyncableDataSet<E> dataSet, List<SyncRequestOperation<E>> syncRequestOps) {
TypedQuery<E> query = dataSet.createQuery(em);
Map<Object, E> localResults = new HashMap<Object, E>();
for (E localEntity : query.getResultList()) {
localResults.put(id(localEntity), localEntity);
}
// maps the old remote ID -> new local persistent entity
Map<Object, E> newLocalEntities = new HashMap<Object, E>();
// the response we will return
List<SyncResponse<E>> syncResponse = new ArrayList<SyncResponse<E>>();
for (SyncRequestOperation<E> syncReq : syncRequestOps) {
// the new state desired by the client. Can be null (for example, entity was remotely deleted).
final E remoteNewState = syncReq.getEntity();
// the expected state (last thing this client saw from us). Can be null (for example, entity was remotely created).
final E remoteExpectedState = syncReq.getExpectedState();
// the JPA ID of the remote entity, whether new to us or known before
final Object remoteId;
if (remoteNewState != null) {
remoteId = id(remoteNewState);
}
else if (remoteExpectedState != null) {
remoteId = id(remoteExpectedState);
}
else {
throw new IllegalArgumentException("New and Expected states can't both be null");
}
// our actual local copy of the entity (null if it has been deleted)
final E localState = localResults.get(remoteId);
// TODO handle related entities reachable from the given ones
switch (syncReq.getType()) {
case UPDATED:
localResults.remove(remoteId);
if (entityComparator.isDifferent(localState, remoteExpectedState)) {
syncResponse.add(new ConflictResponse<E>(remoteExpectedState, localState, remoteNewState));
}
else {
syncResponse.add(new UpdateResponse<E>(em.merge(remoteNewState)));
}
break;
case NEW:
clearId(remoteNewState);
em.persist(remoteNewState);
newLocalEntities.put(remoteId, remoteNewState);
break;
case UNCHANGED:
if (localState == null) {
syncResponse.add(new DeleteResponse<E>(remoteExpectedState));
}
else {
localResults.remove(remoteId);
if (entityComparator.isDifferent(localState, remoteExpectedState)) {
syncResponse.add(new UpdateResponse<E>(localState));
}
}
break;
case DELETED:
// have to check for null in case someone else already deleted this entity
if (localState != null) {
// FIXME need to compare expected state with actual; issue conflict if they differ
localResults.remove(remoteId);
em.remove(localState);
syncResponse.add(new DeleteResponse<E>(localState));
}
break;
default:
throw new UnsupportedOperationException("Unknown sync request type: " + syncReq.getType());
}
}
em.flush();
// pick up new IDs (this has to be done after the flush)
for (Map.Entry<Object, E> newLocalEntity : newLocalEntities.entrySet()) {
syncResponse.add(new IdChangeResponse<E>(newLocalEntity.getKey(), newLocalEntity.getValue()));
}
for (E newOnThisSide : localResults.values()) {
syncResponse.add(new NewRemoteEntityResponse<E>(newOnThisSide));
}
return syncResponse;
}
/**
* Returns the ID of the given object, which must be a JPA entity.
*
* @param entity
* the JPA entity whose ID value to retrieve
* @return The ID of the given entity. If the entity ID type is primitive (for
* example, {@code int} as opposed to {@code Integer}), the
* corresponding boxed value will be returned.
*/
private <X> Object id(X entity) {
// XXX probably need to pass in the actual entity class rather than this cast
// (because dynamic proxies will fool it)
@SuppressWarnings("unchecked")
EntityType<X> type = em.getMetamodel().entity((Class<X>) entity.getClass());
SingularAttribute<? super X, ?> attr = type.getId(type.getIdType().getJavaType());
return attributeAccessor.get(attr, entity);
}
/**
* Sets the ID of the given object, which must be a JPA entity, to its default
* value. The default value for reference types is {@code null}; the default
* value for primitive types is the same as the JVM default value for an
* uninitialized field.
*
* @param entity
* the JPA entity whose ID value to clear
*/
private <X> void clearId(X entity) {
// XXX probably need to pass in the actual entity class rather than this cast
// (because dynamic proxies will fool it)
@SuppressWarnings("unchecked")
EntityType<X> type = em.getMetamodel().entity((Class<X>) entity.getClass());
SingularAttribute<? super X, ?> attr = type.getId(type.getIdType().getJavaType());
attributeAccessor.set(attr, entity, null);
}
}