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 */
019package org.nuxeo.ecm.core.storage.sql;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028
029import org.apache.commons.collections.map.AbstractReferenceMap;
030import org.apache.commons.collections.map.ReferenceMap;
031import org.nuxeo.runtime.metrics.MetricsService;
032
033import com.codahale.metrics.Counter;
034import com.codahale.metrics.MetricRegistry;
035import com.codahale.metrics.SharedMetricRegistries;
036import com.codahale.metrics.Timer;
037
038/**
039 * A {@link SelectionContext} holds information for a set {@link Selection} objects, mostly acting as a cache.
040 * <p>
041 * Some of the information is identical to what's in the database and can be safely be GC'ed, so it lives in a
042 * memory-sensitive map (softMap), otherwise it's moved to a normal map (hardMap) (creation or deletion).
043 */
044public class SelectionContext {
045
046    private final SelectionType selType;
047
048    private final Serializable criterion;
049
050    private final RowMapper mapper;
051
052    private final PersistenceContext context;
053
054    private final Map<Serializable, Selection> softMap;
055
056    // public because used from unit tests
057    public final Map<Serializable, Selection> hardMap;
058
059    /**
060     * The selections modified in the transaction, that should be propagated as invalidations to other sessions at
061     * post-commit time.
062     */
063    private final Set<Serializable> modifiedInTransaction;
064
065    // @since 5.7
066    protected final MetricRegistry registry = SharedMetricRegistries.getOrCreate(MetricsService.class.getName());
067
068    protected final Counter modifiedInTransactionCount;
069
070    protected final Counter cacheHitCount;
071
072    protected final Timer cacheGetTimer;
073
074    @SuppressWarnings("unchecked")
075    public SelectionContext(SelectionType selType, Serializable criterion, RowMapper mapper, PersistenceContext context) {
076        this.selType = selType;
077        this.criterion = criterion;
078        this.mapper = mapper;
079        this.context = context;
080        softMap = new ReferenceMap(AbstractReferenceMap.HARD, AbstractReferenceMap.SOFT);
081        hardMap = new HashMap<Serializable, Selection>();
082        modifiedInTransaction = new HashSet<Serializable>();
083        modifiedInTransactionCount = registry.counter(MetricRegistry.name("nuxeo", "repositories",
084                context.session.repository.getName(), "caches", "selections", "modified"));
085        cacheHitCount = registry.counter(MetricRegistry.name("nuxeo", "repositories",
086                context.session.repository.getName(), "caches", "selections", "hit"));
087        cacheGetTimer = registry.timer(MetricRegistry.name("nuxeo", "repositories",
088                context.session.repository.getName(), "caches", "selections", "get"));
089    }
090
091    public int clearCaches() {
092        // only the soft selections are caches, the others hold info
093        int n = softMap.size();
094        softMap.clear();
095        modifiedInTransactionCount.dec(modifiedInTransaction.size());
096        modifiedInTransaction.clear();
097        return n;
098    }
099
100    public int getSize() {
101        return softMap == null ? 0 : softMap.size();
102    }
103
104    /** Gets the proper selection cache. Creates one if missing. */
105    private Selection getSelection(Serializable selId) {
106        final Timer.Context timerContext = cacheGetTimer.time();
107        try {
108            Selection selection = softMap.get(selId);
109            if (selection != null) {
110                cacheHitCount.inc();
111                return selection;
112            }
113            selection = hardMap.get(selId);
114            if (selection != null) {
115                cacheHitCount.inc();
116                return selection;
117            }
118        } finally {
119            timerContext.stop();
120        }
121
122        return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap);
123    }
124
125    public boolean applicable(SimpleFragment fragment) {
126        // check table name
127        if (!fragment.row.tableName.equals(selType.tableName)) {
128            return false;
129        }
130        // check criterion if there's one
131        if (selType.criterionKey != null) {
132            Serializable crit = fragment.get(selType.criterionKey);
133            if (!criterion.equals(crit)) {
134                return false;
135            }
136        }
137        return true;
138    }
139
140    /**
141     * Records the fragment as a just-created selection member.
142     */
143    public void recordCreated(SimpleFragment fragment) {
144        Serializable id = fragment.getId();
145        // add as a new fragment in the selection
146        Serializable selId = fragment.get(selType.selKey);
147        if (selId != null) {
148            getSelection(selId).addCreated(id);
149            modifiedInTransaction.add(selId);
150            modifiedInTransactionCount.inc();
151        }
152    }
153
154    /**
155     * Notes that a new empty selection should be created.
156     */
157    public void newSelection(Serializable selId) {
158        new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap);
159    }
160
161    /**
162     * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.)
163     *            and must notified to other session; {@code false} if this is a normal read
164     */
165    public void recordExisting(SimpleFragment fragment, boolean invalidate) {
166        Serializable selId = fragment.get(selType.selKey);
167        if (selId != null) {
168            getSelection(selId).addExisting(fragment.getId());
169            if (invalidate) {
170                modifiedInTransaction.add(selId);
171                modifiedInTransactionCount.inc();
172            }
173        }
174    }
175
176    /** Removes a selection item from the selection. */
177    public void recordRemoved(SimpleFragment fragment) {
178        recordRemoved(fragment.getId(), fragment.get(selType.selKey));
179    }
180
181    /** Removes a selection item from the selection. */
182    public void recordRemoved(Serializable id, Serializable selId) {
183        if (selId != null) {
184            getSelection(selId).remove(id);
185            modifiedInTransaction.add(selId);
186            modifiedInTransactionCount.inc();
187        }
188    }
189
190    /** Records a selection as removed. */
191    public void recordRemovedSelection(Serializable selId) {
192        softMap.remove(selId);
193        hardMap.remove(selId);
194        modifiedInTransaction.add(selId);
195        modifiedInTransactionCount.inc();
196    }
197
198    /**
199     * Find a fragment given its selection id and value.
200     * <p>
201     * If the fragment is not in the context, fetch it from the mapper.
202     *
203     * @param selId the selection id
204     * @param filter the value to filter on
205     * @return the fragment, or {@code null} if not found
206     */
207    public SimpleFragment getSelectionFragment(Serializable selId, String filter) {
208        SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter);
209        if (fragment == SimpleFragment.UNKNOWN) {
210            // read it through the mapper
211            List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true);
212            Row row = rows.isEmpty() ? null : rows.get(0);
213            fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false);
214        }
215        return fragment;
216    }
217
218    /**
219     * Finds all the selection fragments for a given id.
220     * <p>
221     * No sorting on value is done.
222     *
223     * @param selId the selection id
224     * @param filter the value to filter on, or {@code null} for all
225     * @return the list of fragments
226     */
227    public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) {
228        Selection selection = getSelection(selId);
229        List<SimpleFragment> fragments = selection.getFragmentsByValue(filter);
230        if (fragments == null) {
231            // no complete list is known
232            // ask the actual selection to the mapper
233            List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false);
234            List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false);
235            fragments = new ArrayList<SimpleFragment>(frags.size());
236            List<Serializable> ids = new ArrayList<Serializable>(frags.size());
237            for (Fragment fragment : frags) {
238                fragments.add((SimpleFragment) fragment);
239                ids.add(fragment.getId());
240            }
241            selection.addExistingComplete(ids);
242
243            // redo the query, as the selection may include newly-created ones,
244            // and we also filter by name
245            fragments = selection.getFragmentsByValue(filter);
246        }
247        return fragments;
248    }
249
250    public void postSave() {
251        // flush selection caches (moves from hard to soft)
252        for (Selection selection : hardMap.values()) {
253            selection.flush(); // added to soft map
254        }
255        hardMap.clear();
256    }
257
258    /**
259     * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore).
260     */
261    public void markInvalidated(Set<RowId> modified) {
262        for (RowId rowId : modified) {
263            if (selType.invalidationTableName.equals(rowId.tableName)) {
264                Serializable id = rowId.id;
265                Selection selection = softMap.get(id);
266                if (selection != null) {
267                    selection.setIncomplete();
268                }
269                selection = hardMap.get(id);
270                if (selection != null) {
271                    selection.setIncomplete();
272                }
273                modifiedInTransaction.add(id);
274                modifiedInTransactionCount.inc();
275            }
276        }
277    }
278
279    /**
280     * Gathers invalidations from this session.
281     * <p>
282     * Called post-transaction to gathers invalidations to be sent to others.
283     */
284    public void gatherInvalidations(Invalidations invalidations) {
285        for (Serializable id : modifiedInTransaction) {
286            invalidations.addModified(new RowId(selType.invalidationTableName, id));
287        }
288        modifiedInTransactionCount.dec(modifiedInTransaction.size());
289        modifiedInTransaction.clear();
290    }
291
292    /**
293     * Processes all invalidations accumulated.
294     * <p>
295     * Called pre-transaction.
296     */
297    public void processReceivedInvalidations(Set<RowId> modified) {
298        for (RowId rowId : modified) {
299            if (selType.invalidationTableName.equals(rowId.tableName)) {
300                Serializable id = rowId.id;
301                softMap.remove(id);
302                hardMap.remove(id);
303            }
304        }
305    }
306
307}