CachedListenerObjectRetriever.java

/*
 * Copyright (C) 2012-2024 RRiBbit.org
 *
 * 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
 *
 * https://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 org.rribbit.retrieval;

import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.rribbit.ListenerObject;
import org.rribbit.creation.ListenerObjectCreator;
import org.rribbit.creation.notification.ListenerObjectCreationObserver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This {@link ListenerObjectRetriever} implements caching to be able to retrieve {@link ListenerObject}s more quickly. It keeps a local {@link Map} where requests are
 * mapped to {@link Collection}s of {@link ListenerObject}s. This class extends from {@link DefaultListenerObjectRetriever} and adds caching to the retrieval methods, except
 * {@link #getListenerObjects()}, because that one simply returns all {@link ListenerObject}s.
 *
 * This class implements the {@link ListenerObjectCreationObserver} in order to clear the cache if any new {@link ListenerObject}s are created by any of the {@link ListenerObjectCreator}s
 * that are associated with this {@link CachedListenerObjectRetriever}. Also, if you add a {@link ListenerObjectCreator} to this {@link CachedListenerObjectRetriever}, the cache will
 * be cleared as well.
 *
 * @author G.J. Schouten
 *
 */
public class CachedListenerObjectRetriever extends DefaultListenerObjectRetriever implements ListenerObjectCreationObserver {

	private static final Logger log = LoggerFactory.getLogger(CachedListenerObjectRetriever.class);

	/**
	 * The cache of {@link Collection}s of {@link ListenerObject}s.
	 */
	protected Map<RetrievalRequest, Collection<ListenerObject>> cache;

	/**
	 * Whenever you use this constructor, be sure to set the {@link ListenerObjectCreator} with the setter provided by this class.
	 * If you don't, runtime {@link NullPointerException}s will occur.
	 */
	public CachedListenerObjectRetriever() {
		this.init();
	}

	/**
	 * This constructor is recommended, since it forces you to specify the {@link ListenerObjectCreator}. Passing a null value for this
	 * will result in runtime {@link NullPointerException}s.
	 *
	 * @param listenerObjectCreators
	 */
	public CachedListenerObjectRetriever(ListenerObjectCreator... listenerObjectCreators) {
		super(listenerObjectCreators);
		this.init();
	}

	/**
	 * Initializes the cache of this {@link CachedListenerObjectRetriever}.
	 */
	protected void init() {
		cache = new ConcurrentHashMap<>();
	}

	/**
	 * Clears the cache.
	 *
	 * @param addedClass
	 */
	@Override
	public void onClassAdded(Class<?> addedClass) {

		log.debug("Class was added by ListenerObjectCreator, clearing cache...");
		this.clearCache();
	}

	@Override
	public Collection<ListenerObject> getListenerObjects(Class<?> returnType) {

		this.checkReturnType(returnType);

		log.debug("Inspecting cache for matches");
		RetrievalRequest request = new RetrievalRequest(null, returnType);
		Collection<ListenerObject> listenerObjects = cache.get(request);
		if(listenerObjects == null) {
			log.debug("No match found, retrieving ListenerObject from DefaultRetriever and storing in cache");
			listenerObjects = super.getListenerObjects(returnType);
			cache.put(request, listenerObjects);
		}
		log.debug("Found {} ListenerObjects", listenerObjects.size());
		return listenerObjects;
	}

	@Override
	public Collection<ListenerObject> getListenerObjects(String hint) {

		this.checkHint(hint);

		log.debug("Inspecting cache for matches");
		RetrievalRequest request = new RetrievalRequest(hint, null);
		Collection<ListenerObject> listenerObjects = cache.get(request);
		if(listenerObjects == null) {
			log.debug("No match found, retrieving ListenerObject from DefaultRetriever and storing in cache");
			listenerObjects = super.getListenerObjects(hint);
			cache.put(request, listenerObjects);
		}
		log.debug("Found {} ListenerObjects", listenerObjects.size());
		return listenerObjects;
	}

	@Override
	public Collection<ListenerObject> getListenerObjects(Class<?> returnType, String hint) {

		this.checkReturnType(returnType);
		this.checkHint(hint);

		log.debug("Inspecting cache for matches");
		RetrievalRequest request = new RetrievalRequest(hint, returnType);
		Collection<ListenerObject> listenerObjects = cache.get(request);
		if(listenerObjects == null) {
			log.debug("No match found, retrieving ListenerObject from DefaultRetriever and storing in cache");
			listenerObjects = super.getListenerObjects(returnType, hint);
			cache.put(request, listenerObjects);
		}
		log.debug("Found {} ListenerObjects", listenerObjects.size());
		return listenerObjects;
	}

	@Override
	public void addListenerObjectCreator(ListenerObjectCreator listenerObjectCreator) {
		super.addListenerObjectCreator(listenerObjectCreator);
		listenerObjectCreator.registerObserver(this);
		this.clearCache();
	}

	@Override
	public void setListenerObjectCreator(ListenerObjectCreator listenerObjectCreator) {
		super.setListenerObjectCreator(listenerObjectCreator);
		listenerObjectCreator.registerObserver(this);
		this.clearCache();
	}

	/**
	 * Checks whether the hint is null, in order to be able to fullfill the {@link ListenerObjectRetriever} contract.
	 *
	 * @param hint
	 */
	protected void checkHint(String hint) {

		if(hint == null) {
			throw new IllegalArgumentException("hint cannot be null!");
		}
	}

	/**
	 * Checks whether the returntype is null, in order to be able to fullfill the {@link ListenerObjectRetriever} contract.
	 *
	 * @param returnType
	 */
	protected void checkReturnType(Class<?> returnType) {

		if(returnType == null) {
			throw new IllegalArgumentException("returnType cannot be null!");
		}
	}

	protected void clearCache() {

		if(cache != null) { //Cache might be null while constructor of superclass is running. This method could be called indirectly from there, so checking for null.
			cache.clear();
		}
	}

	/**
	 * This class represents the request made to this {@link CachedListenerObjectRetriever}. It is used as a key in the {@link Map} in order to quickly
	 * retrieve {@link ListenerObject}s.
	 *
	 * @author G.J. Schouten
	 *
	 */
	protected static class RetrievalRequest {

		private String hint;
		private Class<?> returnType;

		public RetrievalRequest(String hint, Class<?> returnType) {
			this.hint = hint;
			this.returnType = returnType;
		}

		@Override
		public int hashCode() {

			int prime = 31;
			int result = 1;
			result = prime * result + ((hint == null) ? 0 : hint.hashCode());
			result = prime * result + ((returnType == null) ? 0 : returnType.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {

			if(this == obj) {
				return true;
			}
			if(obj == null) {
				return false;
			}
			if(this.getClass() != obj.getClass()) {
				return false;
			}

			RetrievalRequest other = (RetrievalRequest) obj;
			if(hint == null) {
				if(other.hint != null) {
					return false;
				}
			} else if(!hint.equals(other.hint)) {
				return false;
			}
			if(returnType == null) {
				if(other.returnType != null) {
					return false;
				}
			} else if(!returnType.equals(other.returnType)) {
				return false;
			}
			return true;
		}
	}
}