001    /**
002     * Copyright (C) 2009-2011 the original author or authors.
003     * See the notice.md file distributed with this work for additional
004     * information regarding copyright ownership.
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package org.fusesource.restygwt.client.cache;
020    
021    import java.util.Arrays;
022    import java.util.HashMap;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.logging.Logger;
026    
027    import com.google.gwt.core.client.GWT;
028    import com.google.gwt.http.client.Header;
029    import com.google.gwt.http.client.RequestCallback;
030    import com.google.gwt.http.client.Response;
031    import com.google.gwt.logging.client.LogConfiguration;
032    
033    /**
034     * this implementation stores Response objects until they are removed or purged. when retrieved from
035     * the cache the Response will have an extra header field "X-Resty-Cache". this allows CallbackFilter to
036     * determine the action on whether the Response came from the cache or just came over the wire.
037     *
038     * @author kristian
039     *
040     */
041    public class DefaultQueueableCacheStorage implements QueueableCacheStorage {
042    
043        static class ResponseWrapper extends Response {
044    
045            // keep it package private for testing
046            Response response;
047    
048            public boolean equals(Object obj) {
049                return response.equals(obj);
050            }
051    
052            public String getHeader(String header) {
053                if(RESTY_CACHE_HEADER.equals(header)){
054                    return "true";
055                }
056                return response.getHeader(header);
057            }
058    
059            public Header[] getHeaders() {
060                List<Header> headers = Arrays.asList(response.getHeaders());
061                headers.add(new Header() {
062    
063                    @Override
064                    public String getValue() {
065                        return "true";
066                    }
067    
068                    @Override
069                    public String getName() {
070                        return RESTY_CACHE_HEADER;
071                    }
072                });
073                return (Header[]) headers.toArray();
074            }
075    
076            public String getHeadersAsString() {
077                return response.getHeadersAsString() + RESTY_CACHE_HEADER + "=true\r\n";
078            }
079    
080            public int getStatusCode() {
081                return response.getStatusCode();
082            }
083    
084            public String getStatusText() {
085                return response.getStatusText();
086            }
087    
088            public String getText() {
089                return response.getText();
090            }
091    
092            public int hashCode() {
093                return response.hashCode();
094            }
095    
096            public String toString() {
097                return response.toString();
098            }
099    
100            ResponseWrapper(Response resp){
101                this.response = resp;
102            }
103        }
104    
105        private static final String DEFAULT_SCOPE = "";
106    
107        /**
108         * key <> value hashmap for holding cache values. nothing special here.
109         *
110         * invalidated values will be dropped by timer
111         */
112        protected final Map<String, HashMap<CacheKey, Response>> cache =
113                new HashMap<String, HashMap<CacheKey, Response>>();
114    
115        private final Map<CacheKey, List<RequestCallback>> pendingCallbacks =
116                new HashMap<CacheKey, List<RequestCallback>>();
117    
118        public Response getResultOrReturnNull(CacheKey key) {
119            return getResultOrReturnNull(key, DEFAULT_SCOPE);
120        }
121    
122        public Response getResultOrReturnNull(final CacheKey key, final String scope) {
123            final HashMap<CacheKey, Response> scoped = cache.get(scope);
124            if (null != scoped) {
125                Response result = scoped.get(key);
126                if(result != null){
127                    return new ResponseWrapper(result);
128                }
129            }
130    
131            return null;
132        }
133    
134        @Override
135        public void putResult(final CacheKey key, final Response response) {
136            putResult(key, response, DEFAULT_SCOPE);
137        }
138    
139    
140        protected void putResult(final CacheKey key, final Response response, final String scope) {
141            HashMap<CacheKey, Response> scoped = cache.get(scope);
142    
143            if (null == scoped) {
144                cache.put(scope, new HashMap<CacheKey, Response>());
145                scoped = cache.get(scope);
146            }
147    
148            scoped.put(key, response);
149        }
150    
151        @Override
152        public void putResult(CacheKey key, Response response, String... scopes) {
153            if (null == scopes) {
154                putResult(key, response);
155                return;
156            }
157    
158            // TODO mark multi-scoped values as one invalidation group
159            // TODO remove redundant storage
160            for (String scope : scopes) {
161                putResult(key, response, scope);
162            }
163        }
164    
165    
166        @Override
167        public boolean hasCallback(final CacheKey k) {
168            return pendingCallbacks.containsKey(k);
169        }
170    
171        @Override
172        public void addCallback(final CacheKey k, final RequestCallback rc) {
173            //init value of key if not there...
174            if (!pendingCallbacks.containsKey(k)) {
175                pendingCallbacks.put(k, new java.util.LinkedList<RequestCallback>());
176            }
177    
178            // just add callbacks which are not already there
179            if(!pendingCallbacks.get(k).contains(rc)){
180                pendingCallbacks.get(k).add(rc);
181            }
182        }
183    
184        @Override
185        public List<RequestCallback> removeCallbacks(final CacheKey k) {
186            return pendingCallbacks.remove(k);
187        }
188    
189        @Override
190        public void purge() {
191            if (GWT.isClient() && LogConfiguration.loggingIsEnabled()) {
192                Logger.getLogger(DefaultQueueableCacheStorage.class.getName()).finer("remove "
193                        + cache.size() + " elements from cache.");
194            }
195            cache.clear();
196        }
197    
198        @Override
199        public void purge(final String scope) {
200            HashMap<CacheKey, Response> scoped = cache.get(scope);
201    
202            // TODO handle timers in scoping too
203            if(null != scoped) scoped.clear();
204        }
205    
206        @Override
207        public void remove(CacheKey key) {
208            doRemove(key, DEFAULT_SCOPE);
209        }
210    
211        @Override
212        public void remove(CacheKey key, String... scopes) {
213            if(scopes != null){
214                for(String scope: scopes){
215                    doRemove(key, scope);
216                }
217            }
218        }
219    
220        private void doRemove(CacheKey key, String scope){
221            if (GWT.isClient() && LogConfiguration.loggingIsEnabled()) {
222                Logger.getLogger(DefaultQueueableCacheStorage.class.getName())
223                        .finer(
224                                "removing cache-key " + key + " from scope \""
225                                        + scope + "\"");
226            }
227    
228            HashMap<CacheKey, Response> scoped = cache.get(scope);
229            if(null != scoped) scoped.remove(key);
230        }
231    }