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}