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    /**
136     * Gets the ids in that fragment, if complete (otherwise returns {@code null}).
137     *
138     * @since 9.2
139     */
140    public Set<Serializable> getIds() {
141        if (!complete) {
142            return null;
143        }
144        Set<Serializable> ids = new HashSet<>();
145        if (existing != null) {
146            ids.addAll(existing);
147        }
148        if (created != null) {
149            ids.addAll(created);
150        }
151        return ids;
152    }
153
154    protected Serializable fragmentValue(SimpleFragment fragment) {
155        return fragment.get(filterKey);
156    }
157
158    /**
159     * Adds a known row corresponding to the clause.
160     *
161     * @param id the fragment id
162     */
163    public void addExisting(Serializable id) {
164        if (existing == null) {
165            existing = new HashSet<Serializable>();
166        }
167        if (existing.contains(id) || (created != null && created.contains(id))) {
168            // the id is already known here, this happens if the fragment was
169            // GCed from pristine and we had to refetched it from the mapper
170            return;
171        }
172        existing.add(id);
173        warnIfBig(1);
174    }
175
176    /**
177     * Adds a created row corresponding to the clause.
178     *
179     * @param id the fragment id
180     */
181    public void addCreated(Serializable id) {
182        if (created == null) {
183            created = new HashSet<Serializable>();
184            // move to hard map
185            softMap.remove(selId);
186            hardMap.put(selId, this);
187        }
188        if ((existing != null && existing.contains(id)) || created.contains(id)) {
189            // TODO remove sanity check if ok
190            log.error("Creating already present id: " + id);
191            return;
192        }
193        created.add(id);
194    }
195
196    /**
197     * Adds ids actually read from the backend, and mark this complete.
198     * <p>
199     * Note that when adding a complete list of ids retrieved from the database, the deleted ids have already been
200     * removed in the result set.
201     *
202     * @param actualExisting the existing database ids (the list must be mutable)
203     */
204    public void addExistingComplete(List<Serializable> actualExisting) {
205        assert !complete;
206        complete = true;
207        existing = new HashSet<Serializable>(actualExisting);
208    }
209
210    /**
211     * Marks as incomplete.
212     * <p>
213     * Called after a database operation added rows corresponding to the clause with unknown ids (restore of complex
214     * properties).
215     */
216    public void setIncomplete() {
217        complete = false;
218    }
219
220    /**
221     * Removes a known child id.
222     *
223     * @param id the id to remove
224     */
225    public void remove(Serializable id) {
226        if (created != null && created.remove(id)) {
227            // don't add to deleted
228            return;
229        }
230        if (existing != null) {
231            existing.remove(id);
232        }
233        if (deleted == null) {
234            deleted = new HashSet<Serializable>();
235            // move to hard map
236            softMap.remove(selId);
237            hardMap.put(selId, this);
238        }
239        deleted.add(id);
240    }
241
242    /**
243     * Flushes to database. Clears created and deleted map.
244     * <p>
245     * Puts this in the soft map. Caller must remove from hard map.
246     */
247    public void flush() {
248        if (created != null) {
249            if (existing == null) {
250                existing = new HashSet<Serializable>();
251            }
252            existing.addAll(created);
253            warnIfBig(created.size());
254            created = null;
255        }
256        deleted = null;
257        // move to soft map
258        // caller responsible for removing from hard map
259        softMap.put(selId, this);
260    }
261
262    protected void warnIfBig(int added) {
263        if (context.bigSelWarnThreshold != 0) {
264            int size = existing.size();
265            if (size / context.bigSelWarnThreshold != (size - added) / context.bigSelWarnThreshold) {
266                log.warn("Selection " + tableName + "." + filterKey + " for id=" + selId
267                        + " is getting big and now has size: " + size, new RuntimeException("Debug stack trace"));
268            }
269        }
270    }
271
272    public boolean isFlushed() {
273        return created == null && deleted == null;
274    }
275
276    private SimpleFragment getFragmentIfPresent(Serializable id) {
277        RowId rowId = new RowId(tableName, id);
278        return (SimpleFragment) context.getIfPresent(rowId);
279    }
280
281    private SimpleFragment getFragment(Serializable id) {
282        RowId rowId = new RowId(tableName, id);
283        return (SimpleFragment) context.get(rowId, false);
284    }
285
286    /**
287     * Gets a fragment given its filtered value.
288     * <p>
289     * Returns {@code null} if there is no such fragment.
290     * <p>
291     * Returns {@link SimpleFragment#UNKNOWN} if there's no info about it.
292     *
293     * @param filter the value to filter on (cannot be {@code null})
294     * @return the fragment, or {@code null}, or {@link SimpleFragment#UNKNOWN}
295     */
296    public SimpleFragment getFragmentByValue(Serializable filter) {
297        if (existing != null) {
298            for (Serializable id : existing) {
299                SimpleFragment fragment = getFragment(id);
300                if (fragment == null) {
301                    log.warn("Existing fragment missing: " + id);
302                    continue;
303                }
304                if (filter.equals(fragmentValue(fragment))) {
305                    return fragment;
306                }
307            }
308        }
309        if (created != null) {
310            for (Serializable id : created) {
311                SimpleFragment fragment = getFragmentIfPresent(id);
312                if (fragment == null) {
313                    log.warn("Created fragment missing: " + id);
314                    continue;
315                }
316                if (filter.equals(fragmentValue(fragment))) {
317                    return fragment;
318                }
319            }
320        }
321        if (deleted != null) {
322            for (Serializable id : deleted) {
323                SimpleFragment fragment = getFragmentIfPresent(id);
324                if (fragment == null) {
325                    // common case
326                    continue;
327                }
328                if (filter.equals(fragmentValue(fragment))) {
329                    return null;
330                }
331            }
332        }
333        return complete ? null : SimpleFragment.UNKNOWN;
334    }
335
336    /**
337     * Gets all the fragments, if the selection is complete.
338     *
339     * @param filter the value to filter on, or {@code null} for the whole selection
340     * @return the fragments, or {@code null} if the list is not known to be complete
341     */
342    public List<SimpleFragment> getFragmentsByValue(Serializable filter) {
343        if (!complete) {
344            return null;
345        }
346        // fetch fragments and maybe filter
347        List<SimpleFragment> filtered = new LinkedList<SimpleFragment>();
348        if (existing != null) {
349            for (Serializable id : existing) {
350                SimpleFragment fragment = getFragment(id);
351                if (fragment == null) {
352                    log.warn("Existing fragment missing: " + id);
353                    continue;
354                }
355                if (filter == null || filter.equals(fragmentValue(fragment))) {
356                    filtered.add(fragment);
357                }
358            }
359        }
360        if (created != null) {
361            for (Serializable id : created) {
362                SimpleFragment fragment = getFragmentIfPresent(id);
363                if (fragment == null) {
364                    log.warn("Created fragment missing: " + id);
365                    continue;
366                }
367                if (filter == null || filter.equals(fragmentValue(fragment))) {
368                    filtered.add(fragment);
369                }
370            }
371        }
372        return filtered;
373    }
374
375}