SoftCache.java

/*
 * Copyright (c) 2007-2024 MetaSolutions AB
 *
 * 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 org.entrystore.impl;

import lombok.Getter;
import lombok.Setter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.entrystore.Entry;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;


//TODO prioritize recently used objects so they are not
// garbage collected first. Simple use is to have hard references
// to everything used within the last 30 minutes, but that does
// take into account amount of available memory.
public class SoftCache {

	@Getter
	private final HashMap<URI, SoftReference<Entry>> cache = new HashMap<>();

	HashMap<URI, Object> uri2entryURIs = new HashMap<>();

	Thread remover;

	ReferenceQueue<Entry> clearedRefs;

	Log log = LogFactory.getLog(SoftCache.class);

	@Getter
	@Setter
	private boolean shutdown = false;

	public SoftCache() {
		clearedRefs = new ReferenceQueue<>();

		// start thread to delete cleared references from the cache
		remover = new Remover(clearedRefs, this);
		remover.start();

		// add a shutdown hook to interrupt the endless loop
		// the hook is only called when the whole VM is shutdown
		Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
	}

	public void clear() {
		synchronized (cache) {
			cache.clear();
			uri2entryURIs.clear();
		}
	}

	public void put(Entry entry) {
		synchronized (cache) {
			URI entryURI = entry.getEntryURI();
			cache.put(entryURI, new SoftReference<>(entry, clearedRefs));
			push(entry.getLocalMetadataURI(), entryURI);
			push(entry.getExternalMetadataURI(), entryURI);
			push(entry.getResourceURI(), entryURI);
			push(entry.getRelationURI(), entryURI);
		}
	}

	private void push(URI from, URI to) {
		if (from == null || to == null) {
			return;
		}
		Object existingTo = uri2entryURIs.get(from);
		if (existingTo == null) {
			uri2entryURIs.put(from, to);
		} else {
			if (existingTo instanceof Set) {
				((Set<URI>) existingTo).add(to);
			} else {
				HashSet<URI> set = new HashSet<>();
				set.add((URI) existingTo);
				set.add(to);
				uri2entryURIs.put(from, set);
			}
		}
	}

	private void pop(URI from, URI to) {
		if (from == null || to == null) {
			return;
		}
		Object existingTo = uri2entryURIs.get(from);
		if (existingTo != null) {
			if (existingTo instanceof Set) {
				((Set) existingTo).remove(to);
				if (((Set) existingTo).isEmpty()) {
					uri2entryURIs.remove(from);
				}
			} else if (existingTo.equals(to)){
				uri2entryURIs.remove(from);
			}
		}
	}

	public void remove(Entry entry) {
		if(entry == null) return;
		synchronized (cache) {
			URI entryURI = entry.getEntryURI();
			cache.remove(entryURI);
			pop(entry.getLocalMetadataURI(), entryURI);
			pop(entry.getExternalMetadataURI(), entryURI);
			pop(entry.getResourceURI(), entryURI);
			pop(entry.getRelationURI(), entryURI);
		}
	}

	public Entry getByEntryURI(URI uri) {
		SoftReference<Entry> sr = cache.get(uri);

		if (sr != null) {
			return sr.get();
		}
		return null;
	}

	public Set<Entry> getByURI(URI uri) {
		if (uri2entryURIs.containsKey(uri)) {
			HashSet<Entry> entries = new HashSet<>();
			Object entryUris = uri2entryURIs.get(uri);
			if (entryUris instanceof Set) {
				for (URI entryURI : ((Set<URI>) entryUris)) {
					entries.add(getByEntryURI(entryURI));
				}
			} else {
				entries.add(getByEntryURI((URI) entryUris));
			}
			return entries;
		}
		return null;
	}

	public void shutdown() {
		if (remover == null || (shutdown && remover.isInterrupted())) {
			return;
		}
		log.info("Shutting down SoftCache");
		setShutdown(true);
		remover.interrupt();
	}

	private class Remover extends Thread {

		ReferenceQueue<Entry> refQ;

		SoftCache cache;

		public Remover(ReferenceQueue<Entry> rq, SoftCache cache) {
			super();
			this.refQ = rq;
			this.cache = cache;
			setDaemon(true);
		}

		public void run() {
			try {
				while (!shutdown) {
					Reference ref = refQ.remove();
					cache.remove((Entry) ref.get());
				}
			} catch (InterruptedException e) {
				log.info("SoftCache remover got interrupted, shutting down");
			}
		}

	}
}