001package gu.sql2java;
002
003import java.util.Arrays;
004import java.util.Map.Entry;
005import java.util.concurrent.ConcurrentMap;
006import java.util.concurrent.ExecutionException;
007import java.util.concurrent.TimeUnit;
008
009import com.google.common.base.Joiner;
010import com.google.common.base.Strings;
011import com.google.common.base.Throwables;
012import com.google.common.cache.CacheBuilder;
013import com.google.common.cache.DeepCacheBuilder;
014import com.google.common.cache.CacheLoader;
015import com.google.common.cache.LoadingCache;
016import com.google.common.cache.RemovalListener;
017import com.google.common.cache.RemovalNotification;
018import com.google.common.collect.ImmutableMap;
019import com.google.common.util.concurrent.UncheckedExecutionException;
020
021import gu.sql2java.exception.ObjectRetrievalException;
022import gu.sql2java.exception.RuntimeDaoException;
023
024import static com.google.common.base.Preconditions.*;
025import static gu.sql2java.SimpleLog.*;
026import static com.google.common.base.MoreObjects.*;
027
028
029/**
030 * 基于 {@link LoadingCache}实现表数据缓存,并可以通过{@link TableListener}实现缓存数据自动更新<br>
031 * 支持一个或多个column组成的唯一索引
032 * @author guyadong
033 *
034 * @param <B> 数据库记录对象类型(Java Bean)
035 */
036public class ColumnCache<B extends BaseBean>extends TableListener.Adapter<B> implements IKeyCache<B>,Constant {
037        protected final RowMetaData metaData;
038        private final LoadingCache<Object[], B> cache;
039    private final ConcurrentMap<Object[], B> cacheMap;
040    /** 当前更新策略 */
041    protected final UpdateStrategy updateStrategy;
042    protected final Long maximumSize;
043    protected final long duration;
044    protected final TimeUnit unit;
045        private final int[] keyIds;
046        protected final BaseTableManager<B> manager;
047        protected final String indexName;
048        protected static boolean debug = false;
049    /**
050     * 构造函数
051     * @param metaData 表元数据
052     * @param indexName 索引名, 为null时视为primary key
053     * @param updateStrategy 缓存更新策略
054     * @param maximumSize 最大缓存容量,参见 {@link CacheBuilder#maximumSize(long)}
055     * @param duration 失效时间,参见 {@link CacheBuilder#expireAfterWrite(long, TimeUnit)}
056     * @param unit {@code duration}的时间单位
057     */
058    ColumnCache(RowMetaData metaData,String indexName,UpdateStrategy updateStrategy, long maximumSize, long duration, TimeUnit unit) {
059        this.metaData = checkNotNull(metaData,"metaData is null");
060                this.manager = BaseTableManager.getTableManager(metaData.tablename);
061                this.indexName = Strings.emptyToNull(indexName);
062        if(this.indexName == null){
063                this.keyIds = metaData.primaryKeyIds;
064        }else{
065                this.keyIds = metaData.indexIdArray(indexName);
066        }
067
068        this.updateStrategy = firstNonNull(updateStrategy, DEFAULT_STRATEGY);
069        this.maximumSize = maximumSize > 0 ? maximumSize : DEFAULT_CACHE_MAXIMUMSIZE;
070        this.duration = duration > 0 ? duration : DEFAULT_DURATION;
071        this.unit = firstNonNull(unit, DEFAULT_TIME_UNIT);
072        cache = DeepCacheBuilder.newBuilder()
073            .maximumSize(this.maximumSize)
074            .expireAfterWrite(this.duration, this.unit)
075            .removalListener(new RemovalListener<Object[],B>(){
076                                @Override
077                                public void onRemoval(RemovalNotification<Object[], B> notification) {
078                                        if(debug){
079                                                log("CACHE REMOVE:Key:{} for {}", 
080                                                                Arrays.toString(notification.getKey()),
081                                                                ColumnCache.this.metaData.tablename);
082                                        }                                       
083                                }})
084            .build(
085                new CacheLoader<Object[],B>() {
086                    @Override
087                    public B load(Object[] keys) throws Exception {
088                        return loadfromDatabase(keys);
089                    }});
090        cacheMap = cache.asMap();
091        if(debug){
092                log("ColumnCache FOR %s(%s) of %s(%s)",
093                        firstNonNull(indexName, "PK"),
094                        Joiner.on(",").join(metaData.columnNamesOf(keyIds)),                    
095                        metaData.tablename,
096                        updateStrategy);
097        }
098    }
099    /**
100     * @param objects
101     * @return first index of element that not null if exists,or return -1 if not found,or -2 if objects is null 
102     */
103    private static int indexOfFirstNull(Object...objects) {
104                if(objects != null){
105                        for(int i = 0; i < objects.length; ++i){
106                                if(null == objects[i]){
107                                        return i;
108                                }
109                        }
110                        return -1;
111                }
112                return -2;
113        }
114    /**
115         * @param objects object array
116         * @return true if objects is null or any element in objects is null 
117         */
118        private static boolean hasNull(Object...objects) {
119                return indexOfFirstNull(objects) != -1;
120        }
121    /**
122     * check the keys's value is valid for index or primary key 
123     * @param keys array of key value
124     * @throws ObjectRetrievalException has null element in keys
125     */
126    private void checkNonNullKey(Object...keys)throws ObjectRetrievalException{
127        checkArgument(keys != null && keys.length == keyIds.length,
128                        "MISMATCHED length of 'keys' with column count of %s",                          
129                                firstNonNull(indexName, "PK"));
130        int index = indexOfFirstNull(keys);
131        if(index != -1){
132                throw new ObjectRetrievalException(String.format("value of %s is null", metaData.columnNames.get(keyIds[index])));
133        }
134    }
135    /**
136     * 从数据库中加载外键指定的记录,没有找到指定的记录则抛出异常{@link ObjectRetrievalException}<br>
137     * @param keys
138     * @return B 
139     * @throws RuntimeDaoException
140     * @throws ObjectRetrievalException
141     */
142    private B loadfromDatabase(Object[] keys) throws RuntimeDaoException, ObjectRetrievalException{
143        ImmutableMap.Builder<Integer, Object> builder = ImmutableMap.builder();
144        for(int i = 0 ; i < keyIds.length; ++i){
145                builder.put(keyIds[i], keys[i]);
146        }
147        B bean = manager.createBean().copy(builder.build());
148        if(debug){
149                log("LOAD BY %s%s of %s",firstNonNull(indexName, "PK"),Arrays.toString(keys),metaData.tablename);
150        }
151        return manager.loadUniqueUsingTemplateChecked(bean);
152    }
153        @Override
154        public B getBean(Object... keys)throws ObjectRetrievalException{
155        checkNonNullKey(keys);
156        try {
157                return cache.get(keys);
158        }catch(ExecutionException | UncheckedExecutionException e){
159                if(null != e.getCause()){
160                        Throwables.throwIfInstanceOf(e.getCause(), ObjectRetrievalException.class);
161                        Throwables.throwIfUnchecked(e.getCause());
162                }
163                Throwables.throwIfUnchecked(e);
164                throw new RuntimeException(e);
165        }
166    }
167
168    @Override
169        public B getBeanUnchecked(Object... keys){
170        try{
171            return getBean(keys);
172        }catch(ObjectRetrievalException e){
173            return null;
174        }        
175    }
176
177    /** 从缓存中删除{@code bean}指定的记录 */
178    @Override
179    public void remove(B bean){
180        if(bean != null){
181                Object[] keys = bean.asValueArray(keyIds);
182                        cacheMap.remove(keys);
183        }
184    }
185    /**
186     * 更新{@code bean}到指定的缓存对象{@code cacheMap}
187     * @param bean
188     */
189    @Override
190    public void update(B bean){
191        if(bean != null){
192                Object[] keys = bean.asValueArray(keyIds);
193                if(!hasNull(keys)){
194                        if(UpdateStrategy.refresh == updateStrategy){
195                                B value = updateStrategy.update(cacheMap, new ReloadEntry(keys));
196                                // 更新从数据库重新加载的记录到当前记录
197                                bean.copy(value).resetIsModified();
198                        }else{
199                                updateStrategy.update(cacheMap, new ImmutableEntry<Object[],B>(keys,bean));
200                        }
201                        if(debug){
202                                log("UPDATE RECORD %s%s of %s",firstNonNull(indexName, "PK"),Arrays.toString(keys),metaData.tablename);
203                        }
204                }
205        }
206    }
207        
208        @Override
209    public void afterUpdate(B bean) {
210        update(bean);
211    }
212    
213    @Override
214    public void afterInsert(B bean) {
215        update(bean);
216    }
217    
218    @Override
219    public void afterDelete(B bean) {
220        remove(bean);
221    }
222        @Override
223        public int hashCode() {
224                final int prime = 31;
225                int result = 1;
226                result = prime * result + ((indexName == null) ? 0 : indexName.hashCode());
227                result = prime * result + Arrays.hashCode(keyIds);
228                result = prime * result + ((metaData == null) ? 0 : metaData.hashCode());
229                result = prime * result + ((updateStrategy == null) ? 0 : updateStrategy.hashCode());
230                return result;
231        }
232        @Override
233        public boolean equals(Object obj) {
234                if (this == obj) {
235                        return true;
236                }
237                if (obj == null) {
238                        return false;
239                }
240                if (!(obj instanceof ColumnCache)) {
241                        return false;
242                }
243                ColumnCache<?> other = (ColumnCache<?>) obj;
244                if (indexName == null) {
245                        if (other.indexName != null) {
246                                return false;
247                        }
248                } else if (!indexName.equals(other.indexName)) {
249                        return false;
250                }
251                if (!Arrays.equals(keyIds, other.keyIds)) {
252                        return false;
253                }
254                if (metaData == null) {
255                        if (other.metaData != null) {
256                                return false;
257                        }
258                } else if (!metaData.equals(other.metaData)) {
259                        return false;
260                }
261                if (updateStrategy != other.updateStrategy) {
262                        return false;
263                }
264                return true;
265        }
266        @Override
267        public String toString() {
268                StringBuilder builder = new StringBuilder();
269                builder.append("ColumnCache [tablename=");
270                builder.append(metaData.tablename);
271                builder.append(", updateStrategy=");
272                builder.append(updateStrategy);
273                builder.append(", keyIds=");
274                builder.append(metaData.columnNamesOf(keyIds));
275                builder.append(", indexName=");
276                builder.append(firstNonNull(indexName,"PK"));
277                builder.append("]");
278                return builder.toString();
279        }
280        /**
281         * set debug flag that determine if output log message,default : false
282         * @param debug flag for debug message output
283         */
284        public static void setDebug(boolean debug) {
285                ColumnCache.debug = debug;
286        }
287        private class ImmutableEntry<K,V> implements Entry<K,V>{
288            private final K key;
289            private final V value;
290            public ImmutableEntry(K key) {
291                this(key, null);
292            }
293            public ImmutableEntry(K key, V value) {
294                this.key = key;
295                this.value = value;             
296            }
297            @Override
298            public final K getKey() {
299                return key;
300            }
301
302            @Override
303            public final V getValue() {            
304                try {
305                    return doGetValue();
306                } catch(ObjectRetrievalException e){
307                    return null;
308                }catch (Exception e) {
309                    Throwables.throwIfUnchecked(e);
310                    throw new RuntimeException(e);
311                }
312            }
313            @Override
314            public final V setValue(V value) {
315                throw new UnsupportedOperationException();
316            }
317            protected V doGetValue()throws Exception{
318                return value;
319            }
320        }
321        private class ReloadEntry extends ImmutableEntry<Object[],B>{
322        public ReloadEntry(Object[] key) {
323            super(key);
324        }
325
326        @Override
327        public B doGetValue()throws Exception {
328            return (B) loadfromDatabase(getKey());
329        }        
330    }
331}