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