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}