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