/*
* (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
*
* 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.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;
/**
* A {@link Selection} holds information about row ids corresponding to a fixed clause for a given table.
* <p>
* A clause has the form: column = fixed value. The column can be the parent id, the versionable id, the target id.
* <p>
* The internal state of a {@link Selection} instance reflects:
* <ul>
* <li>corresponding rows known to exist in the database,</li>
* <li>corresponding created rows not yet flushed to database,</li>
* <li>corresponding rows not yet flushed to database.</li>
* </ul>
* Information about rows in the database may be complete, or just partial if only individual rows corresponding to the
* clause have been retrieved from the database.
* <p>
* Row ids 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 Selection {
private static final Log log = LogFactory.getLog(Selection.class);
/**
* The selection id, also the key which this instance has in the map holding it.
* <p>
* For instance for a children selection this is the parent id.
*/
private final Serializable selId;
/**
* The table name to fetch fragment.
*/
private final String tableName;
/**
* The context used to fetch fragments.
*/
protected final PersistenceContext context;
/**
* The key to use to filter.
* <p>
* For instance for a children selection this is the child name.
*/
protected final String filterKey;
/** The map where this is stored when GCable. */
private final Map<Serializable, Selection> softMap;
/** The map where this is stored when not GCable. */
private final Map<Serializable, Selection> hardMap;
/**
* This is {@code true} when complete information about the existing ids is known.
* <p>
* This is the case when a query to the database has been made to fetch all rows with the clause, or when a new
* value for the clause has been created (applies for instance to a new parent id appearing when a folder is
* created).
*/
protected boolean complete;
/**
* The row ids known in the database and not deleted.
*/
protected Set<Serializable> existing;
/** The row ids created and not yet flushed to database. */
protected Set<Serializable> created;
/**
* The row ids deleted (or for which the clause column changed value) and not yet flushed to database.
*/
protected Set<Serializable> deleted;
/**
* Constructs a {@link Selection} for the given selection id.
* <p>
* It is automatically put in the soft map.
*
* @param selId the selection key (used in the soft/hard maps)
* @param tableName the table name to fetch fragments
* @param empty if the new instance is created empty
* @param filterKey the key to use to additionally filter on fragment values
* @param context the context from which to fetch fragments
* @param softMap the soft map, when the selection is pristine
* @param hardMap the hard map, when there are modifications to flush
*/
public Selection(Serializable selId, String tableName, boolean empty, String filterKey, PersistenceContext context,
Map<Serializable, Selection> softMap, Map<Serializable, Selection> hardMap) {
this.selId = selId;
this.tableName = tableName;
this.context = context;
this.filterKey = filterKey;
this.softMap = softMap;
this.hardMap = hardMap;
complete = empty;
// starts its life in the soft map (no created or deleted)
softMap.put(selId, this);
}
protected Serializable fragmentValue(SimpleFragment fragment) {
return fragment.get(filterKey);
}
/**
* Adds a known row corresponding to the clause.
*
* @param id the fragment id
*/
public void addExisting(Serializable id) {
if (existing == null) {
existing = new HashSet<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);
warnIfBig(1);
}
/**
* Adds a created row corresponding to the clause.
*
* @param id the fragment id
*/
public void addCreated(Serializable id) {
if (created == null) {
created = new HashSet<Serializable>();
// move to hard map
softMap.remove(selId);
hardMap.put(selId, 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 = new HashSet<Serializable>(actualExisting);
}
/**
* Marks as incomplete.
* <p>
* Called after a database operation added rows corresponding to the clause 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(selId);
hardMap.put(selId, 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 HashSet<Serializable>();
}
existing.addAll(created);
warnIfBig(created.size());
created = null;
}
deleted = null;
// move to soft map
// caller responsible for removing from hard map
softMap.put(selId, this);
}
protected void warnIfBig(int added) {
if (context.bigSelWarnThreshold != 0) {
int size = existing.size();
if (size / context.bigSelWarnThreshold != (size - added) / context.bigSelWarnThreshold) {
log.warn("Selection " + tableName + "." + filterKey + " for id=" + selId
+ " is getting big and now has size: " + size, new RuntimeException("Debug stack trace"));
}
}
}
public boolean isFlushed() {
return created == null && deleted == null;
}
private SimpleFragment getFragmentIfPresent(Serializable id) {
RowId rowId = new RowId(tableName, id);
return (SimpleFragment) context.getIfPresent(rowId);
}
private SimpleFragment getFragment(Serializable id) {
RowId rowId = new RowId(tableName, id);
return (SimpleFragment) context.get(rowId, false);
}
/**
* Gets a fragment given its filtered value.
* <p>
* Returns {@code null} if there is no such fragment.
* <p>
* Returns {@link SimpleFragment#UNKNOWN} if there's no info about it.
*
* @param filter the value to filter on (cannot be {@code null})
* @return the fragment, or {@code null}, or {@link SimpleFragment#UNKNOWN}
*/
public SimpleFragment getFragmentByValue(Serializable filter) {
if (existing != null) {
for (Serializable id : existing) {
SimpleFragment fragment = getFragment(id);
if (fragment == null) {
log.warn("Existing fragment missing: " + id);
continue;
}
if (filter.equals(fragmentValue(fragment))) {
return fragment;
}
}
}
if (created != null) {
for (Serializable id : created) {
SimpleFragment fragment = getFragmentIfPresent(id);
if (fragment == null) {
log.warn("Created fragment missing: " + id);
continue;
}
if (filter.equals(fragmentValue(fragment))) {
return fragment;
}
}
}
if (deleted != null) {
for (Serializable id : deleted) {
SimpleFragment fragment = getFragmentIfPresent(id);
if (fragment == null) {
// common case
continue;
}
if (filter.equals(fragmentValue(fragment))) {
return null;
}
}
}
return complete ? null : SimpleFragment.UNKNOWN;
}
/**
* Gets all the fragments, if the selection is complete.
*
* @param filter the value to filter on, or {@code null} for the whole selection
* @return the fragments, or {@code null} if the list is not known to be complete
*/
public List<SimpleFragment> getFragmentsByValue(Serializable filter) {
if (!complete) {
return null;
}
// fetch fragments and maybe filter
List<SimpleFragment> filtered = new LinkedList<SimpleFragment>();
if (existing != null) {
for (Serializable id : existing) {
SimpleFragment fragment = getFragment(id);
if (fragment == null) {
log.warn("Existing fragment missing: " + id);
continue;
}
if (filter == null || filter.equals(fragmentValue(fragment))) {
filtered.add(fragment);
}
}
}
if (created != null) {
for (Serializable id : created) {
SimpleFragment fragment = getFragmentIfPresent(id);
if (fragment == null) {
log.warn("Created fragment missing: " + id);
continue;
}
if (filter == null || filter.equals(fragmentValue(fragment))) {
filtered.add(fragment);
}
}
}
return filtered;
}
}