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 protected Serializable fragmentValue(SimpleFragment fragment) { 136 return fragment.get(filterKey); 137 } 138 139 /** 140 * Adds a known row corresponding to the clause. 141 * 142 * @param id the fragment id 143 */ 144 public void addExisting(Serializable id) { 145 if (existing == null) { 146 existing = new HashSet<Serializable>(); 147 } 148 if (existing.contains(id) || (created != null && created.contains(id))) { 149 // the id is already known here, this happens if the fragment was 150 // GCed from pristine and we had to refetched it from the mapper 151 return; 152 } 153 existing.add(id); 154 warnIfBig(1); 155 } 156 157 /** 158 * Adds a created row corresponding to the clause. 159 * 160 * @param id the fragment id 161 */ 162 public void addCreated(Serializable id) { 163 if (created == null) { 164 created = new HashSet<Serializable>(); 165 // move to hard map 166 softMap.remove(selId); 167 hardMap.put(selId, this); 168 } 169 if ((existing != null && existing.contains(id)) || created.contains(id)) { 170 // TODO remove sanity check if ok 171 log.error("Creating already present id: " + id); 172 return; 173 } 174 created.add(id); 175 } 176 177 /** 178 * Adds ids actually read from the backend, and mark this complete. 179 * <p> 180 * Note that when adding a complete list of ids retrieved from the database, the deleted ids have already been 181 * removed in the result set. 182 * 183 * @param actualExisting the existing database ids (the list must be mutable) 184 */ 185 public void addExistingComplete(List<Serializable> actualExisting) { 186 assert !complete; 187 complete = true; 188 existing = new HashSet<Serializable>(actualExisting); 189 } 190 191 /** 192 * Marks as incomplete. 193 * <p> 194 * Called after a database operation added rows corresponding to the clause with unknown ids (restore of complex 195 * properties). 196 */ 197 public void setIncomplete() { 198 complete = false; 199 } 200 201 /** 202 * Removes a known child id. 203 * 204 * @param id the id to remove 205 */ 206 public void remove(Serializable id) { 207 if (created != null && created.remove(id)) { 208 // don't add to deleted 209 return; 210 } 211 if (existing != null) { 212 existing.remove(id); 213 } 214 if (deleted == null) { 215 deleted = new HashSet<Serializable>(); 216 // move to hard map 217 softMap.remove(selId); 218 hardMap.put(selId, this); 219 } 220 deleted.add(id); 221 } 222 223 /** 224 * Flushes to database. Clears created and deleted map. 225 * <p> 226 * Puts this in the soft map. Caller must remove from hard map. 227 */ 228 public void flush() { 229 if (created != null) { 230 if (existing == null) { 231 existing = new HashSet<Serializable>(); 232 } 233 existing.addAll(created); 234 warnIfBig(created.size()); 235 created = null; 236 } 237 deleted = null; 238 // move to soft map 239 // caller responsible for removing from hard map 240 softMap.put(selId, this); 241 } 242 243 protected void warnIfBig(int added) { 244 if (context.bigSelWarnThreshold != 0) { 245 int size = existing.size(); 246 if (size / context.bigSelWarnThreshold != (size - added) / context.bigSelWarnThreshold) { 247 log.warn("Selection " + tableName + "." + filterKey + " for id=" + selId 248 + " is getting big and now has size: " + size, new RuntimeException("Debug stack trace")); 249 } 250 } 251 } 252 253 public boolean isFlushed() { 254 return created == null && deleted == null; 255 } 256 257 private SimpleFragment getFragmentIfPresent(Serializable id) { 258 RowId rowId = new RowId(tableName, id); 259 return (SimpleFragment) context.getIfPresent(rowId); 260 } 261 262 private SimpleFragment getFragment(Serializable id) { 263 RowId rowId = new RowId(tableName, id); 264 return (SimpleFragment) context.get(rowId, false); 265 } 266 267 /** 268 * Gets a fragment given its filtered value. 269 * <p> 270 * Returns {@code null} if there is no such fragment. 271 * <p> 272 * Returns {@link SimpleFragment#UNKNOWN} if there's no info about it. 273 * 274 * @param filter the value to filter on (cannot be {@code null}) 275 * @return the fragment, or {@code null}, or {@link SimpleFragment#UNKNOWN} 276 */ 277 public SimpleFragment getFragmentByValue(Serializable filter) { 278 if (existing != null) { 279 for (Serializable id : existing) { 280 SimpleFragment fragment = getFragment(id); 281 if (fragment == null) { 282 log.warn("Existing fragment missing: " + id); 283 continue; 284 } 285 if (filter.equals(fragmentValue(fragment))) { 286 return fragment; 287 } 288 } 289 } 290 if (created != null) { 291 for (Serializable id : created) { 292 SimpleFragment fragment = getFragmentIfPresent(id); 293 if (fragment == null) { 294 log.warn("Created fragment missing: " + id); 295 continue; 296 } 297 if (filter.equals(fragmentValue(fragment))) { 298 return fragment; 299 } 300 } 301 } 302 if (deleted != null) { 303 for (Serializable id : deleted) { 304 SimpleFragment fragment = getFragmentIfPresent(id); 305 if (fragment == null) { 306 // common case 307 continue; 308 } 309 if (filter.equals(fragmentValue(fragment))) { 310 return null; 311 } 312 } 313 } 314 return complete ? null : SimpleFragment.UNKNOWN; 315 } 316 317 /** 318 * Gets all the fragments, if the selection is complete. 319 * 320 * @param filter the value to filter on, or {@code null} for the whole selection 321 * @return the fragments, or {@code null} if the list is not known to be complete 322 */ 323 public List<SimpleFragment> getFragmentsByValue(Serializable filter) { 324 if (!complete) { 325 return null; 326 } 327 // fetch fragments and maybe filter 328 List<SimpleFragment> filtered = new LinkedList<SimpleFragment>(); 329 if (existing != null) { 330 for (Serializable id : existing) { 331 SimpleFragment fragment = getFragment(id); 332 if (fragment == null) { 333 log.warn("Existing fragment missing: " + id); 334 continue; 335 } 336 if (filter == null || filter.equals(fragmentValue(fragment))) { 337 filtered.add(fragment); 338 } 339 } 340 } 341 if (created != null) { 342 for (Serializable id : created) { 343 SimpleFragment fragment = getFragmentIfPresent(id); 344 if (fragment == null) { 345 log.warn("Created fragment missing: " + id); 346 continue; 347 } 348 if (filter == null || filter.equals(fragmentValue(fragment))) { 349 filtered.add(fragment); 350 } 351 } 352 } 353 return filtered; 354 } 355 356}