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        Selection selection = getSelectionOrNull(selId);
107        if (selection != null) {
108            return selection;
109        }
110        return new Selection(selId, selType.tableName, false, selType.filterKey, context, softMap, hardMap);
111    }
112
113    /**
114     * Gets the proper selection cache, if it exists, otherwise returns {@code null}.
115     *
116     * @since 9.2
117     */
118    protected Selection getSelectionOrNull(Serializable selId) {
119        final Timer.Context timerContext = cacheGetTimer.time();
120        try {
121            Selection selection = softMap.get(selId);
122            if (selection != null) {
123                cacheHitCount.inc();
124                return selection;
125            }
126            selection = hardMap.get(selId);
127            if (selection != null) {
128                cacheHitCount.inc();
129                return selection;
130            }
131        } finally {
132            timerContext.stop();
133        }
134        return null;
135    }
136
137    public boolean applicable(SimpleFragment fragment) {
138        // check table name
139        if (!fragment.row.tableName.equals(selType.tableName)) {
140            return false;
141        }
142        // check criterion if there's one
143        if (selType.criterionKey != null) {
144            Serializable crit = fragment.get(selType.criterionKey);
145            if (!criterion.equals(crit)) {
146                return false;
147            }
148        }
149        return true;
150    }
151
152    /**
153     * Records the fragment as a just-created selection member.
154     */
155    public void recordCreated(SimpleFragment fragment) {
156        Serializable id = fragment.getId();
157        // add as a new fragment in the selection
158        Serializable selId = fragment.get(selType.selKey);
159        if (selId != null) {
160            getSelection(selId).addCreated(id);
161            modifiedInTransaction.add(selId);
162            modifiedInTransactionCount.inc();
163        }
164    }
165
166    /**
167     * Notes that a new empty selection should be created.
168     */
169    public void newSelection(Serializable selId) {
170        new Selection(selId, selType.tableName, true, selType.filterKey, context, softMap, hardMap);
171    }
172
173    /**
174     * @param invalidate {@code true} if this is for a fragment newly created by internal database process (copy, etc.)
175     *            and must notified to other session; {@code false} if this is a normal read
176     */
177    public void recordExisting(SimpleFragment fragment, boolean invalidate) {
178        Serializable selId = fragment.get(selType.selKey);
179        if (selId != null) {
180            getSelection(selId).addExisting(fragment.getId());
181            if (invalidate) {
182                modifiedInTransaction.add(selId);
183                modifiedInTransactionCount.inc();
184            }
185        }
186    }
187
188    /** Removes a selection item from the selection. */
189    public void recordRemoved(SimpleFragment fragment) {
190        recordRemoved(fragment.getId(), fragment.get(selType.selKey));
191    }
192
193    /** Removes a selection item from the selection. */
194    public void recordRemoved(Serializable id, Serializable selId) {
195        if (selId != null) {
196            getSelection(selId).remove(id);
197            modifiedInTransaction.add(selId);
198            modifiedInTransactionCount.inc();
199        }
200    }
201
202    /** Records a selection as removed. */
203    public void recordRemovedSelection(Serializable selId) {
204        softMap.remove(selId);
205        hardMap.remove(selId);
206        modifiedInTransaction.add(selId);
207        modifiedInTransactionCount.inc();
208    }
209
210    /**
211     * Find a fragment given its selection id and value.
212     * <p>
213     * If the fragment is not in the context, fetch it from the mapper.
214     *
215     * @param selId the selection id
216     * @param filter the value to filter on
217     * @return the fragment, or {@code null} if not found
218     */
219    public SimpleFragment getSelectionFragment(Serializable selId, String filter) {
220        SimpleFragment fragment = getSelection(selId).getFragmentByValue(filter);
221        if (fragment == SimpleFragment.UNKNOWN) {
222            // read it through the mapper
223            List<Row> rows = mapper.readSelectionRows(selType, selId, filter, criterion, true);
224            Row row = rows.isEmpty() ? null : rows.get(0);
225            fragment = (SimpleFragment) context.getFragmentFromFetchedRow(row, false);
226        }
227        return fragment;
228    }
229
230    /**
231     * Finds all the selection fragments for a given id.
232     * <p>
233     * No sorting on value is done.
234     *
235     * @param selId the selection id
236     * @param filter the value to filter on, or {@code null} for all
237     * @return the list of fragments
238     */
239    public List<SimpleFragment> getSelectionFragments(Serializable selId, String filter) {
240        Selection selection = getSelection(selId);
241        List<SimpleFragment> fragments = selection.getFragmentsByValue(filter);
242        if (fragments == null) {
243            // no complete list is known
244            // ask the actual selection to the mapper
245            List<Row> rows = mapper.readSelectionRows(selType, selId, null, criterion, false);
246            List<Fragment> frags = context.getFragmentsFromFetchedRows(rows, false);
247            fragments = new ArrayList<SimpleFragment>(frags.size());
248            List<Serializable> ids = new ArrayList<Serializable>(frags.size());
249            for (Fragment fragment : frags) {
250                fragments.add((SimpleFragment) fragment);
251                ids.add(fragment.getId());
252            }
253            selection.addExistingComplete(ids);
254
255            // redo the query, as the selection may include newly-created ones,
256            // and we also filter by name
257            fragments = selection.getFragmentsByValue(filter);
258        }
259        return fragments;
260    }
261
262    /**
263     * Gets all the selection fragment ids for a given list of values.
264     *
265     * @since 9.2
266     */
267    public Set<Serializable> getSelectionIds(List<Serializable> values) {
268        return mapper.readSelectionsIds(selType, values);
269    }
270
271    public void postSave() {
272        // flush selection caches (moves from hard to soft)
273        for (Selection selection : hardMap.values()) {
274            selection.flush(); // added to soft map
275        }
276        hardMap.clear();
277    }
278
279    /**
280     * Marks locally all the invalidations gathered by a {@link Mapper} operation (like a version restore).
281     */
282    public void markInvalidated(Set<RowId> modified) {
283        for (RowId rowId : modified) {
284            if (selType.invalidationTableName.equals(rowId.tableName)) {
285                Serializable id = rowId.id;
286                Selection selection = softMap.get(id);
287                if (selection != null) {
288                    selection.setIncomplete();
289                }
290                selection = hardMap.get(id);
291                if (selection != null) {
292                    selection.setIncomplete();
293                }
294                modifiedInTransaction.add(id);
295                modifiedInTransactionCount.inc();
296            }
297        }
298    }
299
300    /**
301     * Gathers invalidations from this session.
302     * <p>
303     * Called post-transaction to gathers invalidations to be sent to others.
304     */
305    public void gatherInvalidations(Invalidations invalidations) {
306        for (Serializable id : modifiedInTransaction) {
307            invalidations.addModified(new RowId(selType.invalidationTableName, id));
308        }
309        modifiedInTransactionCount.dec(modifiedInTransaction.size());
310        modifiedInTransaction.clear();
311    }
312
313    /**
314     * Processes all invalidations accumulated.
315     * <p>
316     * Called pre-transaction.
317     */
318    public void processReceivedInvalidations(Set<RowId> modified) {
319        for (RowId rowId : modified) {
320            if (selType.invalidationTableName.equals(rowId.tableName)) {
321                Serializable id = rowId.id;
322                softMap.remove(id);
323                hardMap.remove(id);
324            }
325        }
326    }
327
328}