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