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.db.DB;
022import io.stallion.dataAccess.db.Schema;
023import io.stallion.dataAccess.filtering.FilterCache;
024import io.stallion.dataAccess.filtering.FilterChain;
025import io.stallion.exceptions.ConfigException;
026import io.stallion.exceptions.UsageException;
027import io.stallion.reflection.PropertyUtils;
028import io.stallion.services.Log;
029import org.apache.commons.lang3.StringUtils;
030
031import java.util.*;
032
033import static io.stallion.utils.Literals.*;
034
035
036/**
037 * PartialStash loads the most recently updated 50,000 items in memory, and also
038 * keeps in memory every item directly loaded by id. This is useful for largish tables
039 * with recent data accessed very frequently, but where you don't want to incur
040 * a really long load-time when booting the server during deployment.
041 *
042 * @param <T>
043 */
044public class PartialStash<T extends Model> extends Stash<T> {
045
046    protected Map<Long, T> itemByPrimaryKey;
047    protected List<T> items;
048    protected Set<String> uniqueFields;
049    protected Map<String, Map<Object, T>> keyNameToUniqueKeyToValue;
050    protected List<Col> columns;
051
052    @Override
053    public void init(DataAccessRegistration registration, ModelController<T> controller, Persister<T> persister) {
054        super.init(registration, controller, persister);
055        if (registration.getDynamicModelDefinition() != null) {
056            columns = registration.getDynamicModelDefinition().getColumns();
057        }
058        initialize();
059    }
060
061    private void initialize() {
062        if (getPersister() == null) {
063            throw new ConfigException("A controller must have a persister before it is inited");
064        }
065        if (StringUtils.isEmpty(getBucket())) {
066            throw new ConfigException("A controller must have a valid bucket before it is inited");
067        }
068        FilterCache.initCache(getBucket());
069        this.items = new ArrayList<>();
070        this.uniqueFields = set();
071        this.itemByPrimaryKey = new HashMap<Long, T>();
072        // Use a hashtable to enforce uniqueness
073        this.keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>();
074        for(String key: getUniqueFields()) {
075            this.keyNameToUniqueKeyToValue.put(key, new Hashtable<Object, T>());
076        }
077
078        // Get the unique keys and alternative keys from annotations
079        for(String propertyName: PropertyUtils.getPropertyNames(this.getPersister().getModelClass())) {
080            if (PropertyUtils.propertyHasAnnotation(this.getPersister().getModelClass(), propertyName, UniqueKey.class)) {
081                Log.fine("Model:{0} has uniquekey on {1}", this.getPersister().getModelClass(), propertyName);
082                this.keyNameToUniqueKeyToValue.put(propertyName, new HashMap<Object, T>());
083            }
084        }
085        if (!empty(columns)) {
086            for (Col col: columns) {
087                if (col.getUniqueKey()) {
088                    this.keyNameToUniqueKeyToValue.put(col.getPropertyName(), new HashMap<Object, T>());
089                }
090            }
091        }
092        uniqueFields = this.keyNameToUniqueKeyToValue.keySet();
093    }
094
095
096    @Override
097    public void sync(T obj) {
098        // TODO: exclude properties with annotation @SyncExclude
099        T existing = this.originalForId(obj.getId());
100        if (existing != null) {
101            List<String> changedKeyFields = new ArrayList<>();
102            cloneInto(obj, existing, null, false, changedKeyFields);
103        }
104    }
105
106
107    /**
108     * Clones all non-null values from "source" into "dest"
109     * @param source
110     * @param dest
111     * @param properties
112     * @param copyNulls
113     * @param changedKeyFields
114     */
115    public boolean cloneInto(Object source, Object dest, Iterable<String> properties, Boolean copyNulls, List<String> changedKeyFields) {
116        if (changedKeyFields == null) {
117            changedKeyFields = new ArrayList<String>();
118        }
119        boolean hasChanges = false;
120        if (properties == null){
121            //properties = PropertyUtils.describe(dest).keySet();
122            properties = PropertyUtils.getProperties(dest).keySet();
123        }
124        for(String name: properties) {
125            if (name.equals("id")) {
126                continue;
127            }
128            if (name.equals("class")) {
129                continue;
130            }
131            if (name.equals("controller")) {
132                continue;
133            }
134            Object o = PropertyUtils.getProperty(source, name);
135            Object previous = PropertyUtils.getProperty(dest, name);
136            if (o != null || copyNulls) {
137                if (previous == o || o != null && o.equals(previous)) {
138                    continue;
139                }
140                if (getKeyFields() != null && this.getKeyFields().contains(name) && previous != null && !previous.equals(o)) {
141                    changedKeyFields.add(name);
142                }
143                hasChanges = true;
144                PropertyUtils.setProperty(dest, name, o);
145            }
146        }
147        return hasChanges;
148    }
149
150
151
152    @Override
153    public T detach(T obj) {
154        T existing = this.itemByPrimaryKey.get(obj.getId());
155        if (existing == null) {
156            return obj;
157        }
158        T newItem = null;
159        try {
160            newItem = (T)existing.getClass().newInstance();
161        } catch (Exception e) {
162            throw new RuntimeException(e);
163        }
164        newItem.setId(obj.getId());
165        cloneInto(existing, newItem, null, true, null);
166        return newItem;
167    }
168
169
170    @Override
171    public void save(T obj) {
172        T existing = originalForId(obj.getId());
173        if (existing != null) {
174            syncForSave(obj);
175            getPersister().persist(existing);
176        } else {
177            Map<String, Object> changedValues = map();
178            // If these are not the same reference in memory, then we find the changed values,
179            // and sync values from the detached object into the permanent object
180            if (existing != null && obj != existing) {
181                for (Map.Entry<String, Object> entry: PropertyUtils.getProperties(obj).entrySet()) {
182                    Object org = PropertyUtils.getPropertyOrMappedValue(existing, entry.getKey());
183                    if (org == null && entry.getValue() != null) {
184                        changedValues.put(entry.getKey(), entry.getValue());
185                    } else if (org != null && org.equals(entry.getValue())) {
186                        changedValues.put(entry.getKey(), entry.getValue());
187                    }
188                }
189                this.syncForSave(obj);
190                getPersister().update(obj, changedValues);
191            } else {
192                getPersister().persist(obj);
193            }
194        }
195    }
196
197    @Override
198    public void syncForSave(T obj) {
199        T existing = this.originalForId(obj.getId());
200        List<String> changedKeyFields = new ArrayList<>();
201        cloneInto(obj, existing, null, true, changedKeyFields);
202    }
203
204    @Override
205    public void hardDelete(T obj) {
206        getPersister().hardDelete(obj);
207    }
208
209    @Override
210    public void loadAll() {
211        if (!DB.isUseDummyPersisterForSqlGenerationMode()) {
212            for (T obj : DB.instance().query(getPersister().getModelClass(), getInitialLoadSql())) {
213                loadItem(obj);
214            }
215        }
216    }
217
218    public String getInitialLoadSql() {
219        Schema schema = DB.instance().getSchema(getPersister().getModelClass());
220        String sql = "SELECT * FROM " + schema.getName() + " ORDER BY row_updated_at DESC LIMIT 50000";
221        return sql;
222    }
223
224
225
226
227
228
229    public void loadForId(Long id)  {
230        T item = (T) getPersister().fetchOne(id);
231        loadItem(item);
232        FilterCache.clearBucket(getBucket());
233    }
234
235
236
237    public boolean loadItem(T item)  {
238        //Log.fine("Pojo item: {0}:{1}", item.getClass().getName(), item.getId());
239        boolean hasChanges =  false;
240        if (item.getId() == null) {
241            Log.warn("Loading a pojo item with a null ID! bucket: {0} class:{1}", getBucket(), item.getClass().getName());
242        }
243        T original = itemByPrimaryKey.getOrDefault(item.getId(), null);
244        if (original != null) {
245            hasChanges = cloneInto(item, original, null, true, list());
246            item = original;
247        } else {
248            registerItem(item);
249            hasChanges = true;
250        }
251        getController().onPostLoadItem(item);
252        registerKeys(item);
253        return hasChanges;
254    }
255
256    /**
257     * This gets called before persist, to avoid a race-condition whereby:
258     *
259     * 1) Thread A saves to the database
260     * 2) Thread B syncs from the database and loads the new object
261     * 3) Thread A finishes the database save, and then adds the newly saved item to the registry
262     * 4) Now we have two copies of the object in memory
263     *
264     *
265     *
266     * @param item
267     */
268    protected void preRegisterItem(T item) {
269        itemByPrimaryKey.put(item.getId(), item);
270    }
271
272    protected void registerItem(T item) {
273        itemByPrimaryKey.put(item.getId(), item);
274        items.add(item);
275    }
276
277    public void registerKeys(T item) {
278        for (String uniqueKey : keyNameToUniqueKeyToValue.keySet()) {
279            Object val = PropertyUtils.getPropertyOrMappedValue(item, uniqueKey);
280            if (val != null) {
281                this.keyNameToUniqueKeyToValue.get(uniqueKey).put(val, item);
282            }
283        }
284    }
285
286
287
288
289    @Override
290    public List<T> getItems() {
291        throw new UsageException("This PartialStash does not have all items in memory.");
292    }
293
294
295    @Override
296    public void reset() {
297        items = new ArrayList<>();
298        itemByPrimaryKey = new HashMap<Long, T>();
299        keyNameToUniqueKeyToValue = new Hashtable<String, Map<Object, T>>();
300        initialize();
301        loadAll();
302        FilterCache.clearBucket(getBucket());
303    }
304
305
306
307    @Override
308    public void onPreRead() {
309        getPersister().onPreRead();
310    }
311
312
313    @Override
314    public T forId(Long id) {
315        T item = this.itemByPrimaryKey.get(id);
316        if (item != null) {
317            onPreRead();
318            return detach(item);
319        } else {
320            item = filterChain().filter("id", id).first();
321            if (item != null) {
322                loadItem(item);
323            }
324            if (item == null) {
325                return null;
326            }
327            return detach(item);
328        }
329    }
330
331    @Override
332    public T originalForId(Long id) {
333        T item = this.itemByPrimaryKey.get(id);
334        if (item != null) {
335            onPreRead();
336            return item;
337        } else {
338            return getPersister().fetchOne(id);
339        }
340    }
341
342    @Override
343    public T forUniqueKey(String keyName, Object lookupValue) {
344        Map<Object, T> map = this.keyNameToUniqueKeyToValue.getOrDefault(keyName, null);
345        if (map == null) {
346            throw new ConfigException("There is no unique key '" + keyName + "' defined for bucket '" + getBucket() + "'");
347        }
348        T value = map.get(lookupValue);
349        if (value != null) {
350            onPreRead();
351            return detach(value);
352        } else {
353            return filterChain().filter(keyName, value).first();
354        }
355    }
356
357
358    @Override
359    public List<T> listForKey(String keyName, Object value) {
360        return filterChain().filter(keyName, value).all();
361    }
362
363    @Override
364    public int countForKey(String keyName, Object value) {
365        return filterChain().filter(keyName, value).count();
366    }
367
368    @Override
369    public FilterChain<T> filterChain() {
370        return getPersister().filterChain();
371    }
372
373    @Override
374    public FilterChain<T> filterByKey(String key, Object lookupValue) {
375        return filterChain().filter(key, lookupValue);
376    }
377
378    @Override
379    public FilterChain<T> filterChain(List<T> subset) {
380        return new FilterChain<T>(getBucket(), subset, null);
381    }
382
383
384}