001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.cache;
003
004import java.util.Arrays;
005import java.util.Collections;
006import java.util.HashSet;
007import java.util.Map;
008import java.util.Map.Entry;
009import java.util.Optional;
010import java.util.Set;
011import java.util.concurrent.ConcurrentHashMap;
012
013import org.apache.commons.jcs.engine.ElementAttributes;
014import org.openstreetmap.josm.tools.Logging;
015
016/**
017 * Class that contains attributes for JCS cache entries. Parameters are used to properly handle HTTP caching,
018 * and metadata structures, that should be stored together with the cache entry
019 *
020 * @author Wiktor Niesiobędzki
021 * @since 8168
022 */
023public class CacheEntryAttributes extends ElementAttributes {
024    private static final long serialVersionUID = 1L; //version
025    private final Map<String, String> attrs = new ConcurrentHashMap<>(RESERVED_KEYS.size());
026    private static final String NO_TILE_AT_ZOOM = "noTileAtZoom";
027    private static final String ETAG = "Etag";
028    private static final String LAST_MODIFICATION = "lastModification";
029    private static final String EXPIRATION_TIME = "expirationTime";
030    private static final String HTTP_RESPONSE_CODE = "httpResponseCode";
031    private static final String ERROR_MESSAGE = "errorMessage";
032    private static final String EXCEPTION = "exception";
033    // this contains all of the above
034    private static final Set<String> RESERVED_KEYS = new HashSet<>(Arrays.asList(
035        NO_TILE_AT_ZOOM,
036        ETAG,
037        LAST_MODIFICATION,
038        EXPIRATION_TIME,
039        HTTP_RESPONSE_CODE,
040        ERROR_MESSAGE,
041        EXCEPTION
042    ));
043
044    /**
045     * Constructs a new {@code CacheEntryAttributes}.
046     */
047    public CacheEntryAttributes() {
048        super();
049        attrs.put(NO_TILE_AT_ZOOM, "false");
050        attrs.put(LAST_MODIFICATION, "0");
051        attrs.put(EXPIRATION_TIME, "0");
052        attrs.put(HTTP_RESPONSE_CODE, "200");
053    }
054
055    /**
056     * @return if the entry is marked as "no tile at this zoom level"
057     */
058    public boolean isNoTileAtZoom() {
059        return Boolean.toString(true).equals(attrs.get(NO_TILE_AT_ZOOM));
060    }
061
062    /**
063     * Sets the marker for "no tile at this zoom level"
064     * @param noTileAtZoom true if this entry is "no tile at this zoom level"
065     */
066    public void setNoTileAtZoom(boolean noTileAtZoom) {
067        attrs.put(NO_TILE_AT_ZOOM, Boolean.toString(noTileAtZoom));
068    }
069
070    /**
071     * @return ETag header value, that was returned for this entry.
072     */
073    public String getEtag() {
074        return attrs.get(ETAG);
075    }
076
077    /**
078     * Sets the ETag header that was set with this entry
079     * @param etag Etag header
080     */
081    public void setEtag(String etag) {
082        if (etag != null) {
083            attrs.put(ETAG, etag);
084        }
085    }
086
087    /**
088     * Utility for conversion from String to int, with default to 0, in case of any errors
089     *
090     * @param key - integer as string
091     * @return int value of the string
092     */
093    private long getLongAttr(String key) {
094        try {
095            return Long.parseLong(attrs.computeIfAbsent(key, k -> "0"));
096        } catch (NumberFormatException e) {
097            attrs.put(key, "0");
098            return 0;
099        }
100    }
101
102    /**
103     * @return last modification of the object in cache in milliseconds from Epoch
104     */
105    public long getLastModification() {
106        return getLongAttr(LAST_MODIFICATION);
107    }
108
109    /**
110     * sets last modification of the object in cache
111     *
112     * @param lastModification time in format of milliseconds from Epoch
113     */
114    public void setLastModification(long lastModification) {
115        attrs.put(LAST_MODIFICATION, Long.toString(lastModification));
116    }
117
118    /**
119     * @return when the object expires in milliseconds from Epoch
120     */
121    public long getExpirationTime() {
122        return getLongAttr(EXPIRATION_TIME);
123    }
124
125    /**
126     * sets expiration time for the object in cache
127     *
128     * @param expirationTime in format of milliseconds from epoch
129     */
130    public void setExpirationTime(long expirationTime) {
131        attrs.put(EXPIRATION_TIME, Long.toString(expirationTime));
132    }
133
134    /**
135     * Sets the HTTP response code that was sent with the cache entry
136     *
137     * @param responseCode http status code
138     * @since 8389
139     */
140    public void setResponseCode(int responseCode) {
141        attrs.put(HTTP_RESPONSE_CODE, Integer.toString(responseCode));
142    }
143
144    /**
145     * @return http status code
146     * @since 8389
147     */
148    public int getResponseCode() {
149        return (int) getLongAttr(HTTP_RESPONSE_CODE);
150    }
151
152    /**
153     * Sets the metadata about cache entry. As it stores all data together, with other attributes
154     * in common map, some keys might not be stored.
155     *
156     * @param map metadata to save
157     * @since 8418
158     */
159    public void setMetadata(Map<String, String> map) {
160        for (Entry<String, String> e: map.entrySet()) {
161            if (RESERVED_KEYS.contains(e.getKey())) {
162                Logging.info("Metadata key configuration contains key {0} which is reserved for internal use");
163            } else {
164                attrs.put(e.getKey(), e.getValue());
165            }
166        }
167    }
168
169    /**
170     * Returns an unmodifiable Map containing all metadata. Unmodifiable prevents access to metadata within attributes.
171     *
172     * @return unmodifiable Map with cache element metadata
173     * @since 8418
174     */
175    public Map<String, String> getMetadata() {
176        return Collections.unmodifiableMap(attrs);
177    }
178
179    /**
180     * @return error message returned while retrieving this object
181     */
182    public String getErrorMessage() {
183        return attrs.get(ERROR_MESSAGE);
184    }
185
186    /**
187     * @param error error related to this object
188     * @since 10469
189     */
190    public void setError(Exception error) {
191        setErrorMessage(Logging.getErrorMessage(error));
192    }
193
194    /**
195     * @param message error message related to this object
196     */
197    public void setErrorMessage(String message) {
198        attrs.put(ERROR_MESSAGE, message);
199    }
200
201    /**
202     * @param e exception that caused error
203     *
204     */
205    public void setException(Exception e) {
206        attrs.put(EXCEPTION, e.getClass().getCanonicalName());
207    }
208
209    /**
210     * @return Optional exception that was thrown when fetching resource
211     *
212     */
213    public Optional<Class<? extends Exception>> getException() {
214        String className = attrs.get(EXCEPTION);
215        if (className == null) {
216            return Optional.empty();
217        }
218        try {
219            Class<?> klass = Class.forName(className);
220            if (Exception.class.isAssignableFrom(klass)) {
221                return Optional.of(klass.asSubclass(Exception.class));
222            }
223        } catch (ClassNotFoundException | ClassCastException ex) {
224            Logging.trace(ex);
225        }
226        return Optional.empty();
227    }
228}