001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 */
012
013package org.nuxeo.ecm.core.storage.sql;
014
015import java.io.Serializable;
016import java.util.HashSet;
017import java.util.LinkedList;
018import java.util.List;
019import java.util.Map;
020import java.util.Set;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024
025/**
026 * A {@link Selection} holds information about row ids corresponding to a fixed clause for a given table.
027 * <p>
028 * A clause has the form: column = fixed value. The column can be the parent id, the versionable id, the target id.
029 * <p>
030 * The internal state of a {@link Selection} instance reflects:
031 * <ul>
032 * <li>corresponding rows known to exist in the database,</li>
033 * <li>corresponding created rows not yet flushed to database,</li>
034 * <li>corresponding rows not yet flushed to database.</li>
035 * </ul>
036 * Information about rows in the database may be complete, or just partial if only individual rows corresponding to the
037 * clause have been retrieved from the database.
038 * <p>
039 * Row ids are stored in no particular order.
040 * <p>
041 * When this structure holds information all flushed to the database, then it can safely be GC'ed, so it lives in a
042 * memory-sensitive map (softMap), otherwise it's moved to a normal map (hardMap).
043 * <p>
044 * This class is not thread-safe and should be used only from a single-threaded session.
045 */
046public class Selection {
047
048    private static final Log log = LogFactory.getLog(Selection.class);
049
050    /**
051     * The selection id, also the key which this instance has in the map holding it.
052     * <p>
053     * For instance for a children selection this is the parent id.
054     */
055    private final Serializable selId;
056
057    /**
058     * The table name to fetch fragment.
059     */
060    private final String tableName;
061
062    /**
063     * The context used to fetch fragments.
064     */
065    protected final PersistenceContext context;
066
067    /**
068     * The key to use to filter.
069     * <p>
070     * For instance for a children selection this is the child name.
071     */
072    protected final String filterKey;
073
074    /** The map where this is stored when GCable. */
075    private final Map<Serializable, Selection> softMap;
076
077    /** The map where this is stored when not GCable. */
078    private final Map<Serializable, Selection> hardMap;
079
080    /**
081     * This is {@code true} when complete information about the existing ids is known.
082     * <p>
083     * This is the case when a query to the database has been made to fetch all rows with the clause, or when a new
084     * value for the clause has been created (applies for instance to a new parent id appearing when a folder is
085     * created).
086     */
087    protected boolean complete;
088
089    /**
090     * The row ids known in the database and not deleted.
091     */
092    protected Set<Serializable> existing;
093
094    /** The row ids created and not yet flushed to database. */
095    protected Set<Serializable> created;
096
097    /**
098     * The row ids deleted (or for which the clause column changed value) and not yet flushed to database.
099     */
100    protected Set<Serializable> deleted;
101
102    /**
103     * Constructs a {@link Selection} for the given selection id.
104     * <p>
105     * It is automatically put in the soft map.
106     *
107     * @param selId the selection key (used in the soft/hard maps)
108     * @param tableName the table name to fetch fragments
109     * @param empty if the new instance is created empty
110     * @param filterKey the key to use to additionally filter on fragment values
111     * @param context the context from which to fetch fragments
112     * @param softMap the soft map, when the selection is pristine
113     * @param hardMap the hard map, when there are modifications to flush
114     */
115    public Selection(Serializable selId, String tableName, boolean empty, String filterKey, PersistenceContext context,
116            Map<Serializable, Selection> softMap, Map<Serializable, Selection> hardMap) {
117        this.selId = selId;
118        this.tableName = tableName;
119        this.context = context;
120        this.filterKey = filterKey;
121        this.softMap = softMap;
122        this.hardMap = hardMap;
123        complete = empty;
124        // starts its life in the soft map (no created or deleted)
125        softMap.put(selId, this);
126    }
127
128    protected Serializable fragmentValue(SimpleFragment fragment) {
129        return fragment.get(filterKey);
130    }
131
132    /**
133     * Adds a known row corresponding to the clause.
134     *
135     * @param id the fragment id
136     */
137    public void addExisting(Serializable id) {
138        if (existing == null) {
139            existing = new HashSet<Serializable>();
140        }
141        if (existing.contains(id) || (created != null && created.contains(id))) {
142            // the id is already known here, this happens if the fragment was
143            // GCed from pristine and we had to refetched it from the mapper
144            return;
145        }
146        existing.add(id);
147        warnIfBig(1);
148    }
149
150    /**
151     * Adds a created row corresponding to the clause.
152     *
153     * @param id the fragment id
154     */
155    public void addCreated(Serializable id) {
156        if (created == null) {
157            created = new HashSet<Serializable>();
158            // move to hard map
159            softMap.remove(selId);
160            hardMap.put(selId, this);
161        }
162        if ((existing != null && existing.contains(id)) || created.contains(id)) {
163            // TODO remove sanity check if ok
164            log.error("Creating already present id: " + id);
165            return;
166        }
167        created.add(id);
168    }
169
170    /**
171     * Adds ids actually read from the backend, and mark this complete.
172     * <p>
173     * Note that when adding a complete list of ids retrieved from the database, the deleted ids have already been
174     * removed in the result set.
175     *
176     * @param actualExisting the existing database ids (the list must be mutable)
177     */
178    public void addExistingComplete(List<Serializable> actualExisting) {
179        assert !complete;
180        complete = true;
181        existing = new HashSet<Serializable>(actualExisting);
182    }
183
184    /**
185     * Marks as incomplete.
186     * <p>
187     * Called after a database operation added rows corresponding to the clause with unknown ids (restore of complex
188     * properties).
189     */
190    public void setIncomplete() {
191        complete = false;
192    }
193
194    /**
195     * Removes a known child id.
196     *
197     * @param id the id to remove
198     */
199    public void remove(Serializable id) {
200        if (created != null && created.remove(id)) {
201            // don't add to deleted
202            return;
203        }
204        if (existing != null) {
205            existing.remove(id);
206        }
207        if (deleted == null) {
208            deleted = new HashSet<Serializable>();
209            // move to hard map
210            softMap.remove(selId);
211            hardMap.put(selId, this);
212        }
213        deleted.add(id);
214    }
215
216    /**
217     * Flushes to database. Clears created and deleted map.
218     * <p>
219     * Puts this in the soft map. Caller must remove from hard map.
220     */
221    public void flush() {
222        if (created != null) {
223            if (existing == null) {
224                existing = new HashSet<Serializable>();
225            }
226            existing.addAll(created);
227            warnIfBig(created.size());
228            created = null;
229        }
230        deleted = null;
231        // move to soft map
232        // caller responsible for removing from hard map
233        softMap.put(selId, this);
234    }
235
236    protected void warnIfBig(int added) {
237        if (context.bigSelWarnThreshold != 0) {
238            int size = existing.size();
239            if (size / context.bigSelWarnThreshold != (size - added) / context.bigSelWarnThreshold) {
240                log.warn("Selection " + tableName + "." + filterKey + " for id=" + selId
241                        + " is getting big and now has size: " + size, new RuntimeException("Debug stack trace"));
242            }
243        }
244    }
245
246    public boolean isFlushed() {
247        return created == null && deleted == null;
248    }
249
250    private SimpleFragment getFragmentIfPresent(Serializable id) {
251        RowId rowId = new RowId(tableName, id);
252        return (SimpleFragment) context.getIfPresent(rowId);
253    }
254
255    private SimpleFragment getFragment(Serializable id) {
256        RowId rowId = new RowId(tableName, id);
257        return (SimpleFragment) context.get(rowId, false);
258    }
259
260    /**
261     * Gets a fragment given its filtered value.
262     * <p>
263     * Returns {@code null} if there is no such fragment.
264     * <p>
265     * Returns {@link SimpleFragment#UNKNOWN} if there's no info about it.
266     *
267     * @param filter the value to filter on (cannot be {@code null})
268     * @return the fragment, or {@code null}, or {@link SimpleFragment#UNKNOWN}
269     */
270    public SimpleFragment getFragmentByValue(Serializable filter) {
271        if (existing != null) {
272            for (Serializable id : existing) {
273                SimpleFragment fragment = getFragment(id);
274                if (fragment == null) {
275                    log.warn("Existing fragment missing: " + id);
276                    continue;
277                }
278                if (filter.equals(fragmentValue(fragment))) {
279                    return fragment;
280                }
281            }
282        }
283        if (created != null) {
284            for (Serializable id : created) {
285                SimpleFragment fragment = getFragmentIfPresent(id);
286                if (fragment == null) {
287                    log.warn("Created fragment missing: " + id);
288                    continue;
289                }
290                if (filter.equals(fragmentValue(fragment))) {
291                    return fragment;
292                }
293            }
294        }
295        if (deleted != null) {
296            for (Serializable id : deleted) {
297                SimpleFragment fragment = getFragmentIfPresent(id);
298                if (fragment == null) {
299                    // common case
300                    continue;
301                }
302                if (filter.equals(fragmentValue(fragment))) {
303                    return null;
304                }
305            }
306        }
307        return complete ? null : SimpleFragment.UNKNOWN;
308    }
309
310    /**
311     * Gets all the fragments, if the selection is complete.
312     *
313     * @param filter the value to filter on, or {@code null} for the whole selection
314     * @return the fragments, or {@code null} if the list is not known to be complete
315     */
316    public List<SimpleFragment> getFragmentsByValue(Serializable filter) {
317        if (!complete) {
318            return null;
319        }
320        // fetch fragments and maybe filter
321        List<SimpleFragment> filtered = new LinkedList<SimpleFragment>();
322        if (existing != null) {
323            for (Serializable id : existing) {
324                SimpleFragment fragment = getFragment(id);
325                if (fragment == null) {
326                    log.warn("Existing fragment missing: " + id);
327                    continue;
328                }
329                if (filter == null || filter.equals(fragmentValue(fragment))) {
330                    filtered.add(fragment);
331                }
332            }
333        }
334        if (created != null) {
335            for (Serializable id : created) {
336                SimpleFragment fragment = getFragmentIfPresent(id);
337                if (fragment == null) {
338                    log.warn("Created fragment missing: " + id);
339                    continue;
340                }
341                if (filter == null || filter.equals(fragmentValue(fragment))) {
342                    filtered.add(fragment);
343                }
344            }
345        }
346        return filtered;
347    }
348
349}