/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.cayenne.access;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.DataObject;
import org.apache.cayenne.DataRow;
import org.apache.cayenne.ObjectId;
import org.apache.cayenne.PersistenceState;
import org.apache.cayenne.Persistent;
import org.apache.cayenne.access.event.SnapshotEvent;
import org.apache.cayenne.configuration.Constants;
import org.apache.cayenne.configuration.RuntimeProperties;
import org.apache.cayenne.event.EventBridge;
import org.apache.cayenne.event.EventManager;
import org.apache.cayenne.event.EventSubject;
import org.apache.cayenne.util.concurrentlinkedhashmap.ConcurrentLinkedHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
/**
* A fixed size cache of DataRows keyed by ObjectId.
*
* @since 1.1
*/
public class DataRowStore implements Serializable {
private static Logger logger = LoggerFactory.getLogger(DataRowStore.class);
/**
* @deprecated since 4.0, never used actually
*/
@Deprecated
public static final String SNAPSHOT_EXPIRATION_PROPERTY = "cayenne.DataRowStore.snapshot.expiration";
/**
* @deprecated since 4.0, use {@link org.apache.cayenne.configuration.Constants#SNAPSHOT_CACHE_SIZE_PROPERTY}
*/
@Deprecated
public static final String SNAPSHOT_CACHE_SIZE_PROPERTY = "cayenne.DataRowStore.snapshot.size";
/**
* @deprecated since 4.0 does nothing. Previously it used to check if need to create {@link EventBridge}.
*/
@Deprecated
public static final String REMOTE_NOTIFICATION_PROPERTY = "cayenne.DataRowStore.remote.notify";
/**
* @deprecated since 4.0 {@link DataRowStoreFactory} establishes {@link EventBridge}.
*/
@Deprecated
public static final String EVENT_BRIDGE_FACTORY_PROPERTY = "cayenne.DataRowStore.EventBridge.factory";
// default property values
public static final long SNAPSHOT_EXPIRATION_DEFAULT = 2 * 60 * 60; // default expiration time is 2 hours
public static final int SNAPSHOT_CACHE_SIZE_DEFAULT = 10000;
@Deprecated
public static final boolean REMOTE_NOTIFICATION_DEFAULT = false;
/**
* @deprecated since 4.0 does nothing.
*/
@Deprecated
public static final String EVENT_BRIDGE_FACTORY_DEFAULT = "org.apache.cayenne.event.JavaGroupsBridgeFactory";
protected String name;
private int maxSize;
protected ConcurrentMap<ObjectId, DataRow> snapshots;
/**
* @deprecated since 4.0 does nothing. Previously it used to check if need to create {@link EventBridge}.
*/
@Deprecated
protected boolean notifyingRemoteListeners;
protected transient EventManager eventManager;
protected transient EventBridge remoteNotificationsHandler;
// IMPORTANT: EventSubject must be an ivar to avoid its deallocation
// too early, and thus disabling events.
protected transient EventSubject eventSubject;
/**
* Creates new DataRowStore with a specified name and a set of properties. If no
* properties are defined, default values are used.
*
* @param name DataRowStore name. Used to identify this DataRowStore in events, etc.
* Can't be null.
* @param properties Properties map used to configure DataRowStore parameters. Can be
* null.
* @param eventManager EventManager that should be used for posting and receiving
* events.
* @since 1.2
*/
public DataRowStore(String name, RuntimeProperties properties, EventManager eventManager) {
if (name == null) {
throw new IllegalArgumentException("DataRowStore name can't be null.");
}
this.name = name;
this.eventSubject = createSubject();
this.eventManager = eventManager;
initWithProperties(properties);
}
private EventSubject createSubject() {
return EventSubject.getSubject(this.getClass(), name);
}
protected void initWithProperties(RuntimeProperties properties) {
// expiration time is never used actually
maxSize = properties.getInt(Constants.SNAPSHOT_CACHE_SIZE_PROPERTY, SNAPSHOT_CACHE_SIZE_DEFAULT);
if (logger.isDebugEnabled()) {
logger.debug("DataRowStore property " + Constants.SNAPSHOT_CACHE_SIZE_PROPERTY + " = " + maxSize);
}
this.snapshots = new ConcurrentLinkedHashMap.Builder<ObjectId, DataRow>()
.maximumWeightedCapacity(maxSize)
.build();
}
protected void setEventBridge(EventBridge eventBridge) {
remoteNotificationsHandler = eventBridge;
}
protected EventBridge getEventBridge() {
return remoteNotificationsHandler;
}
/**
* Updates cached snapshots for the list of objects.
*
* @since 1.2
*/
void snapshotsUpdatedForObjects(List<Persistent> objects, List<? extends DataRow> snapshots, boolean refresh) {
int size = objects.size();
// sanity check
if (size != snapshots.size()) {
throw new IllegalArgumentException(
"Counts of objects and corresponding snapshots do not match. "
+ "Objects count: "
+ objects.size()
+ ", snapshots count: "
+ snapshots.size());
}
Map<ObjectId, DataRow> modified = null;
Object eventPostedBy = null;
for (int i = 0; i < size; i++) {
Persistent object = objects.get(i);
// skip null objects... possible since 3.0 in some EJBQL results
if (object == null) {
continue;
}
// skip HOLLOW objects as they likely were created from partial snapshots
if (object.getPersistenceState() == PersistenceState.HOLLOW) {
continue;
}
ObjectId oid = object.getObjectId();
// add snapshots if refresh is forced, or if a snapshot is
// missing
DataRow cachedSnapshot = this.snapshots.get(oid);
if (refresh || cachedSnapshot == null) {
DataRow newSnapshot = snapshots.get(i);
if (cachedSnapshot != null) {
// use old snapshot if no changes occurred
if (object instanceof DataObject
&& cachedSnapshot.equals(newSnapshot)) {
((DataObject) object).setSnapshotVersion(cachedSnapshot.getVersion());
continue;
} else {
newSnapshot.setReplacesVersion(cachedSnapshot.getVersion());
}
}
if (modified == null) {
modified = new HashMap<>();
eventPostedBy = object.getObjectContext().getGraphManager();
}
modified.put(oid, newSnapshot);
}
}
if (modified != null) {
processSnapshotChanges(
eventPostedBy,
modified,
Collections.<ObjectId>emptyList(),
Collections.<ObjectId>emptyList(),
Collections.<ObjectId>emptyList());
}
}
/**
* Returns current cache size.
*/
public int size() {
return snapshots.size();
}
/**
* Returns maximum allowed cache size.
*/
public int maximumSize() {
return maxSize;
}
/**
* Shuts down any remote notification connections, and clears internal cache.
*/
public void shutdown() {
stopListeners();
clear();
}
/**
* Returns the name of this DataRowStore. Name allows to create EventSubjects for
* event notifications addressed to or sent from this DataRowStore.
*/
public String getName() {
return name;
}
/**
* Sets the name of this DataRowStore. Name allows to create EventSubjects for event
* notifications addressed to or sent from this DataRowStore.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns an EventManager associated with this DataRowStore.
*
* @since 1.2
*/
public EventManager getEventManager() {
return eventManager;
}
/**
* Sets an EventManager associated with this DataRowStore.
*
* @since 1.2
*/
public void setEventManager(EventManager eventManager) {
if (eventManager != this.eventManager) {
stopListeners();
this.eventManager = eventManager;
startListeners();
}
}
/**
* Returns cached snapshot or null if no snapshot is currently cached for the given
* ObjectId.
*/
public DataRow getCachedSnapshot(ObjectId oid) {
return snapshots.get(oid);
}
/**
* Returns EventSubject used by this SnapshotCache to notify of snapshot changes.
*/
public EventSubject getSnapshotEventSubject() {
return eventSubject;
}
/**
* Expires and removes all stored snapshots without sending any notification events.
*/
public void clear() {
snapshots.clear();
}
/**
* Evicts a snapshot from cache without generating any SnapshotEvents.
*/
public void forgetSnapshot(ObjectId id) {
snapshots.remove(id);
}
/**
* Handles remote events received via EventBridge. Performs needed snapshot updates,
* and then resends the event to local listeners.
*/
public void processRemoteEvent(SnapshotEvent event) {
if (event.getSource() != remoteNotificationsHandler) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("remote event: " + event);
}
Collection<ObjectId> deletedSnapshotIds = event.getDeletedIds();
Collection<ObjectId> invalidatedSnapshotIds = event.getInvalidatedIds();
Map<ObjectId, DataRow> diffs = event.getModifiedDiffs();
Collection<ObjectId> indirectlyModifiedIds = event.getIndirectlyModifiedIds();
if (deletedSnapshotIds.isEmpty()
&& invalidatedSnapshotIds.isEmpty()
&& diffs.isEmpty()
&& indirectlyModifiedIds.isEmpty()) {
logger.warn("processRemoteEvent.. bogus call... no changes.");
return;
}
processDeletedIDs(deletedSnapshotIds);
processInvalidatedIDs(invalidatedSnapshotIds);
processUpdateDiffs(diffs);
sendUpdateNotification(
event.getPostedBy(),
diffs,
deletedSnapshotIds,
invalidatedSnapshotIds,
indirectlyModifiedIds);
}
/**
* Processes changes made to snapshots. Modifies internal cache state, and then sends
* the event to all listeners. Source of these changes is usually an ObjectStore.
*/
public void processSnapshotChanges(
Object postedBy,
Map<ObjectId, DataRow> updatedSnapshots,
Collection<ObjectId> deletedSnapshotIds,
Collection<ObjectId> invalidatedSnapshotIds,
Collection<ObjectId> indirectlyModifiedIds) {
// update the internal cache, prepare snapshot event
if (deletedSnapshotIds.isEmpty()
&& invalidatedSnapshotIds.isEmpty()
&& updatedSnapshots.isEmpty()
&& indirectlyModifiedIds.isEmpty()) {
logger.warn("postSnapshotsChangeEvent.. bogus call... no changes.");
return;
}
processDeletedIDs(deletedSnapshotIds);
processInvalidatedIDs(invalidatedSnapshotIds);
Map<ObjectId, DataRow> diffs = processUpdatedSnapshots(updatedSnapshots);
sendUpdateNotification(
postedBy,
diffs,
deletedSnapshotIds,
invalidatedSnapshotIds,
indirectlyModifiedIds);
}
private void processDeletedIDs(Collection<ObjectId> deletedSnapshotIDs) {
// DELETED: evict deleted snapshots
if (!deletedSnapshotIDs.isEmpty()) {
for (ObjectId deletedSnapshotID : deletedSnapshotIDs) {
snapshots.remove(deletedSnapshotID);
}
}
}
private void processInvalidatedIDs(Collection<ObjectId> invalidatedSnapshotIds) {
// INVALIDATED: forget snapshot, treat as expired from cache
if (!invalidatedSnapshotIds.isEmpty()) {
for (ObjectId invalidatedSnapshotId : invalidatedSnapshotIds) {
snapshots.remove(invalidatedSnapshotId);
}
}
}
private Map<ObjectId, DataRow> processUpdatedSnapshots(Map<ObjectId, DataRow> updatedSnapshots) {
Map<ObjectId, DataRow> diffs = null;
// MODIFIED: replace/add snapshots, generate diffs for event
if (!updatedSnapshots.isEmpty()) {
for (Map.Entry<ObjectId, DataRow> entry : updatedSnapshots.entrySet()) {
ObjectId key = entry.getKey();
DataRow newSnapshot = entry.getValue();
DataRow oldSnapshot = snapshots.put(key, newSnapshot);
// generate diff for the updated event, if this not a new
// snapshot
// The following cases should be handled here:
// 1. There is no previously cached snapshot for a given id.
// 2. There was a previously cached snapshot for a given id,
// but it expired from cache and was removed. Currently
// handled as (1); what are the consequences of that?
// 3. There is a previously cached snapshot and it has the
// *same version* as the "replacesVersion" property of the
// new snapshot.
// 4. There is a previously cached snapshot and it has a
// *different version* from "replacesVersion" property of
// the new snapshot. It means that we don't know how to merge
// the two (we don't even know which one is newer due to
// multithreading). Just throw out this snapshot....
if (oldSnapshot != null) {
// case 4 above... have to throw out the snapshot since
// no good options exist to tell how to merge the two.
if (oldSnapshot.getVersion() != newSnapshot.getReplacesVersion()) {
// snapshots can be huge potentially.. so print them only if the
// user is expecting them to be printed
if (logger.isDebugEnabled()) {
logger
.debug("snapshot version changed, don't know what to do... Old: "
+ oldSnapshot
+ ", New: "
+ newSnapshot);
}
forgetSnapshot(key);
continue;
}
DataRow diff = oldSnapshot.createDiff(newSnapshot);
if (diff != null) {
if (diffs == null) {
diffs = new HashMap<>();
}
diffs.put(key, diff);
}
}
}
}
return diffs;
}
private void processUpdateDiffs(Map<ObjectId, DataRow> diffs) {
// apply snapshot diffs
if (!diffs.isEmpty()) {
for (Map.Entry<ObjectId, DataRow> entry : diffs.entrySet()) {
ObjectId key = entry.getKey();
DataRow oldSnapshot = snapshots.remove(key);
if (oldSnapshot == null) {
continue;
}
DataRow newSnapshot = oldSnapshot.applyDiff(entry.getValue());
snapshots.put(key, newSnapshot);
}
}
}
private void sendUpdateNotification(
Object postedBy,
Map<ObjectId, DataRow> diffs,
Collection<ObjectId> deletedSnapshotIDs,
Collection<ObjectId> invalidatedSnapshotIDs,
Collection<ObjectId> indirectlyModifiedIds) {
// do not send bogus events... e.g. inserted objects are not counted
if ((diffs != null && !diffs.isEmpty())
|| (deletedSnapshotIDs != null && !deletedSnapshotIDs.isEmpty())
|| (invalidatedSnapshotIDs != null && !invalidatedSnapshotIDs.isEmpty())
|| (indirectlyModifiedIds != null && !indirectlyModifiedIds.isEmpty())) {
SnapshotEvent event = new SnapshotEvent(
this,
postedBy,
diffs,
deletedSnapshotIDs,
invalidatedSnapshotIDs,
indirectlyModifiedIds);
if (logger.isDebugEnabled()) {
logger.debug("postSnapshotsChangeEvent: " + event);
}
// synchronously notify listeners; leaving it up to the listeners to
// register as "non-blocking" if needed.
eventManager.postEvent(event, getSnapshotEventSubject());
}
}
@Deprecated
public boolean isNotifyingRemoteListeners() {
return notifyingRemoteListeners;
}
@Deprecated
public void setNotifyingRemoteListeners(boolean notifyingRemoteListeners) {
this.notifyingRemoteListeners = notifyingRemoteListeners;
}
// deserialization support
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
in.defaultReadObject();
// restore subjects
this.eventSubject = createSubject();
}
void stopListeners() {
if (eventManager != null) {
eventManager.removeListener(this);
}
if (remoteNotificationsHandler != null) {
try {
remoteNotificationsHandler.shutdown();
} catch (Exception ex) {
logger.info("Exception shutting down EventBridge.", ex);
}
remoteNotificationsHandler = null;
}
}
void startListeners() {
if (eventManager != null) {
if (remoteNotificationsHandler != null) {
try {
// listen to EventBridge ... must add itself as non-blocking listener
// otherwise a deadlock can occur as "processRemoteEvent" will attempt
// to
// obtain a lock on this object when the dispatch queue is locked...
// And
// another commit thread may have this object locked and attempt to
// lock
// dispatch queue
eventManager.addNonBlockingListener(
this,
"processRemoteEvent",
SnapshotEvent.class,
getSnapshotEventSubject(),
remoteNotificationsHandler);
// start EventBridge - it will listen to all event sources for this
// subject
remoteNotificationsHandler.startup(
eventManager,
EventBridge.RECEIVE_LOCAL_EXTERNAL);
} catch (Exception ex) {
throw new CayenneRuntimeException(
"Error initializing DataRowStore.",
ex);
}
}
}
}
}