001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.dataAccess; 019 020import io.stallion.dataAccess.db.Col; 021import io.stallion.dataAccess.filtering.FilterCache; 022import io.stallion.dataAccess.filtering.FilterChain; 023import io.stallion.exceptions.ConfigException; 024import io.stallion.reflection.PropertyUtils; 025import io.stallion.services.Log; 026import org.apache.commons.lang3.StringUtils; 027 028import java.util.*; 029 030import static io.stallion.utils.Literals.*; 031 032/** 033 * {@inheritDoc} 034 * 035 * LocalMemoryStash syncs all items from the datastore into local memory. 036 * It maintains the syncing throughout the life-time of the application. 037 * 038 * Internally, it stores all the items in a list called "items". It has 039 * several different lookup tables for accessing the item by primary key, 040 * by unique key, or by an non-unique key. If you have a lot of items, 041 * it is essential to define keys and only find items by keys, otherwise 042 * you will have to iterate through every single item every time you do a 043 * filter or lookup. 044 * 045 * @param <T> 046 */ 047public class LocalMemoryStash<T extends Model> extends StashBase<T> { 048 049 protected Map<Long, T> itemByPrimaryKey; 050 protected List<T> items; 051 protected Set<String> keyFields; 052 protected Set<String> uniqueFields; 053 protected Map<String, Map<Object, Set<T>>> keyNameToKeyToValue; 054 protected Map<String, Map<Object, T>> keyNameToUniqueKeyToValue; 055 protected List<Col> columns; 056 057 @Override 058 public void init(DataAccessRegistration registration, ModelController<T> controller, Persister<T> persister) { 059 super.init(registration, controller, persister); 060 if (registration.getDynamicModelDefinition() != null) { 061 columns = registration.getDynamicModelDefinition().getColumns(); 062 } 063 initialize(); 064 } 065 066 private void initialize() { 067 if (getPersister() == null) { 068 throw new ConfigException("A controller must have a persister before it is inited"); 069 } 070 if (StringUtils.isEmpty(getBucket())) { 071 throw new ConfigException("A controller must have a valid bucket before it is inited"); 072 } 073 FilterCache.initCache(getBucket()); 074 this.items = new ArrayList<>(); 075 this.keyFields = set(); 076 this.uniqueFields = set(); 077 this.itemByPrimaryKey = new HashMap<Long, T>(); 078 this.keyNameToKeyToValue = new HashMap<String, Map<Object, Set<T>>>(); 079 if (this.getKeyFields() != null) { 080 for (String keyFieldName : this.getKeyFields()) { 081 HashMap<Object, Set<T>> keyToValues = new HashMap<>(); 082 this.keyNameToKeyToValue.put(keyFieldName, keyToValues); 083 } 084 } 085 // Use a hashtable to enforce uniqueness 086 this.keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>(); 087 for(String key: getUniqueFields()) { 088 this.keyNameToUniqueKeyToValue.put(key, new Hashtable<Object, T>()); 089 } 090 091 // Get the unique keys and alternative keys from annotations 092 for(String propertyName: PropertyUtils.getPropertyNames(this.getPersister().getModelClass())) { 093 if (PropertyUtils.propertyHasAnnotation(this.getPersister().getModelClass(), propertyName, UniqueKey.class)) { 094 Log.fine("Model:{0} has uniquekey on {1}", this.getPersister().getModelClass(), propertyName); 095 this.keyNameToUniqueKeyToValue.put(propertyName, new HashMap<Object, T>()); 096 } 097 if (PropertyUtils.propertyHasAnnotation(this.getPersister().getModelClass(), propertyName, AlternativeKey.class)) { 098 Log.fine("Model:{0} has alternativeKey on {1}", this.getPersister().getModelClass(), propertyName); 099 this.keyNameToKeyToValue.put(propertyName, new HashMap<Object, Set<T>>()); 100 } 101 } 102 if (!empty(columns)) { 103 for (Col col: columns) { 104 if (col.getUniqueKey()) { 105 this.keyNameToUniqueKeyToValue.put(col.getPropertyName(), new HashMap<Object, T>()); 106 } else if (col.getAlternativeKey()) { 107 this.keyNameToKeyToValue.put(col.getPropertyName(), new HashMap<Object, Set<T>>()); 108 } 109 110 } 111 } 112 keyFields = this.keyNameToKeyToValue.keySet(); 113 uniqueFields = this.keyNameToUniqueKeyToValue.keySet(); 114 } 115 116 @Override 117 public void syncForSave(T obj) { 118 T existing = this.originalForId(obj.getId()); 119 List<String> changedKeyFields = new ArrayList<>(); 120 cloneInto(obj, existing, null, true, changedKeyFields); 121 } 122 123 @Override 124 public void sync(T obj) { 125 // TODO: exclude properties with annotation @SyncExclude 126 T existing = this.originalForId(obj.getId()); 127 List<String> changedKeyFields = new ArrayList<>(); 128 cloneInto(obj, existing, null, false, changedKeyFields); 129 } 130 131 @Override 132 public T detach(T obj) { 133 T existing = this.itemByPrimaryKey.get(obj.getId()); 134 if (existing == null) { 135 return obj; 136 } 137 T newItem = null; 138 try { 139 newItem = (T)existing.getClass().newInstance(); 140 } catch (Exception e) { 141 throw new RuntimeException(e); 142 } 143 newItem.setId(obj.getId()); 144 cloneInto(existing, newItem, null, true, null); 145 return newItem; 146 } 147 148 149 150 @Override 151 public void save(T obj) { 152 T internal = this.itemByPrimaryKey.getOrDefault(obj.getId(), null); 153 if (internal == null) { 154 try { 155 internal = (T)obj.getClass().newInstance(); 156 } catch (InstantiationException e) { 157 throw new RuntimeException(e); 158 } catch (IllegalAccessException e) { 159 throw new RuntimeException(e); 160 } 161 cloneInto(obj, internal, null, true, null); 162 internal.setId(obj.getId()); 163 if (empty(obj.getId())) { 164 internal.setId(DataAccessRegistry.instance().getTickets().nextId()); 165 internal.setIsNewInsert(true); 166 } 167 preRegisterItem(internal); 168 getPersister().persist(internal); 169 registerItem(internal); 170 registerKeys(internal); 171 obj.setId(internal.getId()); 172 obj.setIsNewInsert(false); 173 cloneInto(internal, obj, null, true, null); 174 } else { 175 176 Map<String, Object> changedValues = map(); 177 // If these are not the same reference in memory, then we find the changed values, 178 // and sync values from the detached object into the permanent object 179 if (internal != null && obj != internal) { 180 for (Map.Entry<String, Object> entry: PropertyUtils.getProperties(obj).entrySet()) { 181 Object org = PropertyUtils.getPropertyOrMappedValue(internal, entry.getKey()); 182 if (org == null && entry.getValue() != null) { 183 changedValues.put(entry.getKey(), entry.getValue()); 184 } else if (org != null && !org.equals(entry.getValue())) { 185 changedValues.put(entry.getKey(), entry.getValue()); 186 } 187 } 188 this.syncForSave(obj); 189 getPersister().update(obj, changedValues); 190 } else { 191 getPersister().persist(obj); 192 } 193 194 } 195 FilterCache.clearBucket(getBucket()); 196 197 } 198 199 200 @Override 201 public void hardDelete(T obj) { 202 getPersister().hardDelete(obj); 203 obj.setDeleted(true); 204 if (itemByPrimaryKey.containsKey(obj.getId())) { 205 sync(obj); 206 itemByPrimaryKey.remove(obj.getId()); 207 } 208 FilterCache.clearBucket(getBucket()); 209 } 210 211 212 /** 213 * Clones all non-null values from "source" into "dest" 214 * @param source 215 * @param dest 216 * @param properties 217 * @param copyNulls 218 * @param changedKeyFields 219 * @return true if there were changes 220 */ 221 public boolean cloneInto(Object source, Object dest, Iterable<String> properties, Boolean copyNulls, List<String> changedKeyFields) { 222 if (changedKeyFields == null) { 223 changedKeyFields = new ArrayList<String>(); 224 } 225 if (properties == null){ 226 //properties = PropertyUtils.describe(dest).keySet(); 227 properties = PropertyUtils.getProperties(dest).keySet(); 228 } 229 boolean hasChanges = false; 230 for(String name: properties) { 231 if (name.equals("id")) { 232 continue; 233 } 234 if (name.equals("class")) { 235 continue; 236 } 237 if (name.equals("controller")) { 238 continue; 239 } 240 Object o = PropertyUtils.getProperty(source, name); 241 Object previous = PropertyUtils.getProperty(dest, name); 242 if (o != null || copyNulls) { 243 if (o == previous || o != null && o.equals(previous)) { 244 continue; 245 } 246 if (getKeyFields() != null && this.getKeyFields().contains(name) && previous != null && !previous.equals(o)) { 247 changedKeyFields.add(name); 248 } 249 hasChanges = true; 250 PropertyUtils.setProperty(dest, name, o); 251 } 252 } 253 return hasChanges; 254 } 255 256 public void loadForId(Long id) { 257 T item = (T) getPersister().fetchOne(id); 258 loadItem(item); 259 FilterCache.clearBucket(getBucket()); 260 } 261 262 @Override 263 public void loadAll() { 264 Log.fine("Load all from {0}. ", getBucket()); 265 List<T> items = this.getPersister().fetchAll(); 266 for(T item: items) { 267 loadItem(item); 268 } 269 } 270 271 272 public boolean loadItem(T item) { 273 //Log.fine("Pojo item: {0}:{1}", item.getClass().getName(), item.getId()); 274 boolean hasChanges = false; 275 if (item.getId() == null) { 276 Log.warn("Loading a pojo item with a null ID! bucket: {0} class:{1}", getBucket(), item.getClass().getName()); 277 } 278 T original = itemByPrimaryKey.getOrDefault(item.getId(), null); 279 if (original != null) { 280 hasChanges = cloneInto(item, original, null, false, list()); 281 item = original; 282 } else { 283 hasChanges = true; 284 registerItem(item); 285 } 286 getController().onPostLoadItem(item); 287 registerKeys(item); 288 return hasChanges; 289 } 290 291 public T reloadIfNewer(T obj) { 292 boolean reloaded = getPersister().reloadIfNewer(obj); 293 if (reloaded) { 294 return forId(obj.getId()); 295 } 296 return obj; 297 } 298 299 300 301 /** 302 * This gets called before persist, to avoid a race-condition whereby: 303 * 304 * 1) Thread A saves to the database 305 * 2) Thread B syncs from the database and loads the new object 306 * 3) Thread A finishes the database save, and then adds the newly saved item to the registry 307 * 4) Now we have two copies of the object in memory 308 * 309 * 310 * 311 * @param item 312 */ 313 protected void preRegisterItem(T item) { 314 itemByPrimaryKey.put(item.getId(), item); 315 } 316 317 protected void registerItem(T item) { 318 itemByPrimaryKey.put(item.getId(), item); 319 items.add(item); 320 } 321 322 public void registerKeys(T item) { 323 if (getKeyFields() != null) { 324 for (String keyField : keyNameToKeyToValue.keySet()) { 325 Object obj = PropertyUtils.getPropertyOrMappedValue(item, keyField); 326 if (obj != null) { 327 if (!this.keyNameToKeyToValue.get(keyField).containsKey(obj)) { 328 this.keyNameToKeyToValue.get(keyField).put(obj, set()); 329 } 330 this.keyNameToKeyToValue.get(keyField).get(obj).add(item); 331 } 332 } 333 } 334 for (String uniqueKey : keyNameToUniqueKeyToValue.keySet()) { 335 Object val = PropertyUtils.getPropertyOrMappedValue(item, uniqueKey); 336 if (val != null) { 337 this.keyNameToUniqueKeyToValue.get(uniqueKey).put(val, item); 338 } 339 } 340 } 341 342 343 @Override 344 public List<T> getItems() { 345 return items; 346 } 347 348 @Override 349 public void reset() { 350 items = new ArrayList<>(); 351 itemByPrimaryKey = new HashMap<Long, T>(); 352 keyNameToKeyToValue = new HashMap<String, Map<Object, Set<T>>>(); 353 keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>(); 354 if (getKeyFields() != null) { 355 for (String keyFieldName : getKeyFields()) { 356 HashMap<Object, Set<T>> keyToValues = new HashMap<>(); 357 keyNameToKeyToValue.put(keyFieldName, keyToValues); 358 } 359 } 360 initialize(); 361 loadAll(); 362 FilterCache.clearBucket(getBucket()); 363 } 364 365 @Override 366 public T forId(Long id) { 367 onPreRead(); 368 T item = this.itemByPrimaryKey.get(id); 369 if (item == null) { 370 return null; 371 } 372 return detach(item); 373 } 374 375 @Override 376 public T originalForId(Long id) { 377 onPreRead(); 378 T item = this.itemByPrimaryKey.get(id); 379 if (item == null) { 380 return null; 381 } 382 return item; 383 } 384 385 @Override 386 public T forUniqueKey(String keyName, Object lookupValue) { 387 onPreRead(); 388 Map<Object, T> map = this.keyNameToUniqueKeyToValue.getOrDefault(keyName, null); 389 if (map == null) { 390 throw new ConfigException("There is no unique key '" + keyName + "' defined for bucket '" + getBucket() + "'"); 391 } 392 T value = map.get(lookupValue); 393 if (value == null) { 394 return value; 395 } 396 return detach(value); 397 } 398 399 @Override 400 public List<T> listForKey(String keyName, Object value) { 401 onPreRead(); 402 Set<T> things = this.keyNameToKeyToValue.get(keyName).getOrDefault(value, Collections.EMPTY_SET); 403 return new ArrayList<T>(things); 404 } 405 406 @Override 407 public int countForKey(String keyName, Object value) { 408 return this.keyNameToKeyToValue.get(keyName).size(); 409 } 410 411 @Override 412 public FilterChain<T> filterChain() { 413 return new FilterChain<T>(getBucket(), getItems(), this); 414 } 415 416 417 @Override 418 public FilterChain<T> filterChain(List<T> subset) { 419 return new FilterChain<T>(getBucket(), subset, this); 420 } 421 422 423 @Override 424 public FilterChain<T> filterByKey(String keyName, Object value) { 425 onPreRead(); 426 Map<Object, Set<T>> keyValMap = this.keyNameToKeyToValue.getOrDefault(keyName, null); 427 if (keyValMap == null) { 428 throw new ConfigException("The key " + keyName + " is not a valid key for the bucket " + getBucket() + 429 ". You must either override getKeyFields() in your controller, or pass in the key during the constructore"); 430 } 431 Set<T> vals = keyValMap.getOrDefault(value, null); 432 // If no values with this key, return an empty filter chain 433 if (vals == null) { 434 Log.finer("No items found for key field {0} key value {1}", keyName, value); 435 return new FilterChain<>(getBucket(), this); 436 } 437 438 FilterChain chain = filterChain(new ArrayList<T>(vals)).filter(keyName, value); 439 return chain; 440 } 441 442 @Override 443 public void onPreRead() { 444 getPersister().onPreRead(); 445 } 446 447 @Override 448 public Set<String> getKeyFields() { 449 return keyFields; 450 } 451 452 453 @Override 454 public Set<String> getUniqueFields() { 455 return uniqueFields; 456 } 457 458 459}