/*
* Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Florent Guillaume
*/
package org.eclipse.ecr.core.storage.sql;
import java.io.Serializable;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.ecr.core.storage.StorageException;
/**
* Holds information about the children of a given parent node. The internal
* state reflects:
* <ul>
* <li>children known to exist in the database,</li>
* <li>created children not yet flushed to database,</li>
* <li>deleted children not yet flushed to database.</li>
* </ul>
* Information about children in the database may be complete, or just partial
* if only individual children have been retrieved from the database.
* <p>
* Children are stored in no particular order.
* <p>
* When this structure holds information all flushed to the database, then it
* can safely be GC'ed, so it lives in a memory-sensitive map (softMap),
* otherwise it's moved to a normal map (hardMap).
* <p>
* This class is not thread-safe and should be used only from a single-threaded
* session.
*/
public class Children {
private static final Log log = LogFactory.getLog(Children.class);
/** The context from which to fetch hierarchy fragments. */
protected final HierarchyContext hierContext;
/** The key to use to filter on names. */
protected final String filterKey;
/**
* This is {@code true} when complete information about the existing
* children is known.
* <p>
* This is the case when a query to the database has been made to fetch all
* children, or when a new parent node with no children has been created.
*/
protected boolean complete;
/** The ids known in the database and not deleted. This list is not ordered. */
protected List<Serializable> existing;
/** The ids created and not yet flushed to database. */
protected List<Serializable> created;
/**
* The ids deleted (or which changed parents) and not yet flushed to
* database.
*/
protected Set<Serializable> deleted;
/** The key which this has in the map holding it. */
private final Serializable mapKey;
/** The map where this is stored when GCable. */
private final Map<Serializable, Children> softMap;
/** The map where this is stored when not GCable. */
private final Map<Serializable, Children> hardMap;
/**
* Constructs a Children cache.
* <p>
* It is automatically put in the soft map.
*
* @param hierContext the context from which to fetch hierarchy fragments
* @param filterKey the key to use to filter on names
* @param empty if the new instance is created empty
* @param mapKey the key to use in the following maps
* @param softMap the soft map, when the children are pristine
* @param hardMap the hard map, when there are modifications to flush
*/
public Children(HierarchyContext hierContext, String filterKey,
boolean empty, Serializable mapKey,
Map<Serializable, Children> softMap,
Map<Serializable, Children> hardMap) {
this.hierContext = hierContext;
this.filterKey = filterKey;
complete = empty;
this.mapKey = mapKey;
this.softMap = softMap;
this.hardMap = hardMap;
// starts its life in the soft map (no created or deleted)
softMap.put(mapKey, this);
}
protected Serializable fragmentValue(SimpleFragment fragment) {
try {
return fragment.get(filterKey);
} catch (StorageException e) {
log.error("Could not fetch value: " + fragment.getId());
return null;
}
}
/**
* Adds a known child.
*
* @param id the fragment id
*/
public void addExisting(Serializable id) {
if (existing == null) {
existing = new LinkedList<Serializable>();
}
if (existing.contains(id) || (created != null && created.contains(id))) {
// the id is already known here, this happens if the fragment was
// GCed from pristine and we had to refetched it from the mapper
return;
}
existing.add(id);
}
/**
* Adds a created child.
*
* @param id the fragment id
*/
public void addCreated(Serializable id) {
if (created == null) {
created = new LinkedList<Serializable>();
// move to hard map
softMap.remove(mapKey);
hardMap.put(mapKey, this);
}
if ((existing != null && existing.contains(id)) || created.contains(id)) {
// TODO remove sanity check if ok
log.error("Creating already present id: " + id);
return;
}
created.add(id);
}
/**
* Adds ids actually read from the backend, and mark this complete.
* <p>
* Note that when adding a complete list of ids retrieved from the database,
* the deleted ids have already been removed in the result set.
*
* @param actualExisting the existing database ids (the list must be
* mutable)
*/
public void addExistingComplete(List<Serializable> actualExisting) {
assert !complete;
complete = true;
existing = actualExisting;
}
/**
* Marks as incomplete.
* <p>
* Called after a database operation added children with unknown ids
* (restore of complex properties).
*/
public void setIncomplete() {
complete = false;
}
/**
* Removes a known child id.
*
* @param id the id to remove
*/
public void remove(Serializable id) {
if (created != null && created.remove(id)) {
// don't add to deleted
return;
}
if (existing != null) {
existing.remove(id);
}
if (deleted == null) {
deleted = new HashSet<Serializable>();
// move to hard map
softMap.remove(mapKey);
hardMap.put(mapKey, this);
}
deleted.add(id);
}
/**
* Flushes to database. Clears created and deleted map.
* <p>
* Puts this in the soft map. Caller must remove from hard map.
*/
public void flush() {
if (created != null) {
if (existing == null) {
existing = new LinkedList<Serializable>();
}
existing.addAll(created);
created = null;
}
deleted = null;
// move to soft map
// caller responsible for removing from hard map
softMap.put(mapKey, this);
}
public boolean isFlushed() {
return created == null && deleted == null;
}
/**
* Gets a fragment given its name.
* <p>
* Returns {@code null} if there is no such child.
* <p>
* Returns {@link SimpleFragment#UNKNOWN} if there's no info about it.
*
* @param value the name
* @return the fragment, or {@code null}, or {@link SimpleFragment#UNKNOWN}
*/
public SimpleFragment getFragmentByValue(Serializable value) {
if (existing != null) {
for (Serializable id : existing) {
SimpleFragment fragment;
try {
fragment = hierContext.getHier(id, false);
} catch (StorageException e) {
log.warn("Failed refetch for: " + id, e);
continue;
}
if (fragment == null) {
log.warn("Existing fragment missing: " + id);
continue;
}
if (value.equals(fragmentValue(fragment))) {
return fragment;
}
}
}
if (created != null) {
for (Serializable id : created) {
SimpleFragment fragment = hierContext.getHierIfPresent(id);
if (fragment == null) {
log.warn("Created fragment missing: " + id);
continue;
}
if (value.equals(fragmentValue(fragment))) {
return fragment;
}
}
}
if (deleted != null) {
for (Serializable id : deleted) {
SimpleFragment fragment = hierContext.getHierIfPresent(id);
if (fragment == null) {
log.warn("Deleted fragment missing: " + id);
continue;
}
if (value.equals(fragmentValue(fragment))) {
return null;
}
}
}
return complete ? null : SimpleFragment.UNKNOWN;
}
/**
* Gets all the fragments, if the list of children is complete.
*
* @param value the name to filter on, or {@code null} for all children
* @return the fragments, or {@code null} if the list is not known to be
* complete
*/
public List<SimpleFragment> getFragmentsByValue(Serializable value) {
if (!complete) {
return null;
}
// fetch fragments and maybe filter by name
List<SimpleFragment> filtered = new LinkedList<SimpleFragment>();
if (existing != null) {
for (Serializable id : existing) {
SimpleFragment fragment;
try {
fragment = hierContext.getHier(id, false);
} catch (StorageException e) {
log.warn("Failed refetch for: " + id, e);
continue;
}
if (fragment == null) {
log.warn("Existing fragment missing: " + id);
continue;
}
if (value == null || value.equals(fragmentValue(fragment))) {
filtered.add(fragment);
}
}
}
if (created != null) {
for (Serializable id : created) {
SimpleFragment fragment = hierContext.getHierIfPresent(id);
if (fragment == null) {
log.warn("Created fragment missing: " + id);
continue;
}
if (value == null || value.equals(fragmentValue(fragment))) {
filtered.add(fragment);
}
}
}
return filtered;
}
}