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