/* * File: ResponseCache.java * * Copyright 2007 Macquarie E-Learning Centre Of Excellence * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package melcoe.fedora.pep; import java.security.MessageDigest; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import melcoe.fedora.util.AttributeComparator; import melcoe.fedora.util.SubjectComparator; import melcoe.xacml.MelcoeXacmlException; import melcoe.xacml.util.ContextUtil; import org.apache.log4j.Logger; import com.sun.xacml.ctx.Attribute; import com.sun.xacml.ctx.RequestCtx; import com.sun.xacml.ctx.Subject; /** * @author nishen@melcoe.mq.edu.au */ public class ResponseCacheImpl implements ResponseCache { private static final Logger log = Logger.getLogger(ResponseCacheImpl.class.getName()); private final ContextUtil contextUtil = new ContextUtil(); private static final int DEFAULT_CACHE_SIZE = 1000; private static final long DEFAULT_TTL = 10 * 60 * 1000; private final int CACHE_SIZE; private long TTL; private Map<String, String> requestCache = null; private Map<String, Long> requestCacheTimeTracker = null; private List<String> requestCacheUsageTracker = null; private MessageDigest digest = null; /** * The default constructor that initialises the cache with default values. * * @throws PEPException */ public ResponseCacheImpl() throws PEPException { this(new Integer(DEFAULT_CACHE_SIZE), new Long(DEFAULT_TTL)); } /** * Constructor that initialises the cache with the size and time to live * values. * * @param size * size of the cache * @param ttl * maximum time for a cache item to be valid in milliseconds * @throws PEPException */ public ResponseCacheImpl(Integer size, Long ttl) throws PEPException { String noCache = System.getenv("PEP_NOCACHE"); if (noCache != null && noCache.toLowerCase().startsWith("t")) { TTL = 0; log.info("PEP_NOCACHE: TTL on responseCache set to 0"); } else { TTL = ttl.longValue(); } CACHE_SIZE = size.intValue(); requestCache = new HashMap<String, String>(CACHE_SIZE); requestCacheTimeTracker = new HashMap<String, Long>(CACHE_SIZE); requestCacheUsageTracker = new ArrayList<String>(CACHE_SIZE); try { digest = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new PEPException("Could not initialize the ResponseCache", e); } } /* * (non-Javadoc) * @see melcoe.fedora.pep.ResponseCache#addCacheItem(java.lang.String, * java.lang.String) */ public void addCacheItem(String request, String response) { String hash = null; try { hash = makeHash(request); // if we have a maxxed cache, remove least used item if (requestCache.size() >= CACHE_SIZE) { String key = requestCacheUsageTracker.remove(0); requestCache.remove(key); if (log.isDebugEnabled()) { log.debug("Purging cache element"); } } requestCache.put(hash, response); requestCacheUsageTracker.add(hash); requestCacheTimeTracker.put(hash, new Long(System .currentTimeMillis())); if (log.isDebugEnabled()) { log.debug("Adding Cache Item (" + requestCache.size() + "/" + requestCacheUsageTracker.size() + "/" + requestCacheTimeTracker.size() + "): " + hash); } } catch (Exception e) { log.warn("Error adding cache item: " + e.getMessage(), e); } } /* * (non-Javadoc) * @see melcoe.fedora.pep.ResponseCache#getCacheItem(java.lang.String) */ public String getCacheItem(String request) { String hash = null; String response = null; try { hash = makeHash(request); if (log.isDebugEnabled()) { log.debug("Getting Cache Item (" + requestCache.size() + "/" + requestCacheUsageTracker.size() + "/" + requestCacheTimeTracker.size() + "): " + hash); } response = requestCache.get(hash); if (response == null) { return null; } // if this item is older than CACHE_ITEM_TTL then we can't use it long usedLast = System.currentTimeMillis() - requestCacheTimeTracker.get(hash).longValue(); if (usedLast > TTL) { requestCache.remove(hash); requestCacheUsageTracker.remove(hash); requestCacheTimeTracker.remove(hash); if (log.isDebugEnabled()) { log.debug("CACHE_ITEM_TTL exceeded: " + hash); } return null; } // we just used this item, move it to the end of the list (items at // beginning get removed...) requestCacheUsageTracker.add(requestCacheUsageTracker .remove(requestCacheUsageTracker.indexOf(hash))); } catch (Exception e) { log.warn("Error getting cache item: " + e.getMessage(), e); response = null; } return response; } /* * (non-Javadoc) * @see melcoe.fedora.pep.ResponseCache#invalidate() */ public void invalidate() { requestCache = new HashMap<String, String>(CACHE_SIZE); requestCacheTimeTracker = new HashMap<String, Long>(CACHE_SIZE); requestCacheUsageTracker = new ArrayList<String>(CACHE_SIZE); } /** * Given a request, this method generates a hash. * * @param request * the request to hash * @return the hash * @throws CacheException */ @SuppressWarnings("unchecked") private String makeHash(String request) throws CacheException { RequestCtx reqCtx = null; try { reqCtx = contextUtil.makeRequestCtx(request); } catch (MelcoeXacmlException pe) { throw new CacheException("Error converting request", pe); } digest.reset(); Set<Attribute> attributes = null; Set<Subject> subjects = new TreeSet(new SubjectComparator()); subjects.addAll(reqCtx.getSubjects()); for (Subject s : subjects) { attributes = new TreeSet(new AttributeComparator()); attributes.addAll(s.getAttributes()); for (Attribute a : attributes) { hashAttribute(a); } } attributes = new TreeSet(new AttributeComparator()); attributes.addAll(reqCtx.getResource()); for (Attribute a : attributes) { hashAttribute(a); } attributes = new TreeSet(new AttributeComparator()); attributes.addAll(reqCtx.getAction()); for (Attribute a : attributes) { hashAttribute(a); } attributes = new TreeSet(new AttributeComparator()); attributes.addAll(reqCtx.getEnvironmentAttributes()); for (Attribute a : attributes) { hashAttribute(a); } byte[] hash = digest.digest(); return byte2hex(hash); } /** * Utility function to add an attribute to the hash digest. * * @param a * the attribute to hash */ private void hashAttribute(Attribute a) { digest.update(a.getId().toString().getBytes()); digest.update(a.getType().toString().getBytes()); digest.update(a.getValue().encode().getBytes()); if (a.getIssuer() != null) { digest.update(a.getIssuer().getBytes()); } if (a.getIssueInstant() != null) { digest.update(a.getIssueInstant().encode().getBytes()); } } /** * Converts a hash into its hexadecimal string representation. * * @param bytes * the byte array to convert * @return the hexadecimal string representation */ private String byte2hex(byte[] bytes) { char[] hexChars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; StringBuffer sb = new StringBuffer(); for (byte b : bytes) { sb.append(hexChars[b >> 4 & 0xf]); sb.append(hexChars[b & 0xf]); } return new String(sb); } }