/******************************************************************************* * Copyright (c) 2016 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.properties.editor.util; import java.time.Duration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.function.Function; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; /** * A cache that automatically removes entries when they reach a certain age. * * @author Kris De Volder */ public class LimitedTimeCache<K,V> implements Cache<K, V> { private static final boolean DEBUG = false; //(""+Platform.getLocation()).contains("kdvolder"); private static void debug(String string) { if (DEBUG) { System.out.println(string); } } public final long MAX_AGE; public final long AGE_MARGIN; private Map<K, Entry> cache = new HashMap<>(); /** * Job that removes expired entries from the cache. */ private Job clearExpiredJob = new Job("Clean expired cache entries") { { setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { long oldest = clearExpired(); //Reschedule job to again run when the next entry expires. if (oldest>=0) { clearExpiredJob.schedule(MAX_AGE - oldest + AGE_MARGIN); } return Status.OK_STATUS; } }; private class Entry { long lastUsed; V value; Entry(V v) { this.value = v; this.lastUsed = System.currentTimeMillis(); } long age() { return System.currentTimeMillis() - lastUsed; } } public LimitedTimeCache(Duration MAX_AGE_DURATION) { MAX_AGE = MAX_AGE_DURATION.toMillis(); AGE_MARGIN = Math.max(1000, MAX_AGE / 20); } @Override public synchronized V get(K key) { Entry e = cache.get(key); if (e!=null) { e.lastUsed = System.currentTimeMillis(); return e.value; } return null; } @Override public synchronized void put(K key, V value) { boolean wasEmpty = cache.isEmpty(); if (value==null) { cache.remove(key); } else { cache.put(key, new Entry(value)); } //if the cache was not empty then it is already guaranteed a 'cleaningJob' will run to // clean the existing oldest entry. if (wasEmpty && !cache.isEmpty()) { //The entry we just added is the only one in the cache. //Therefore, that entry must be the oldest and its age is 0. //So we know when the job should run next: clearExpiredJob.schedule(MAX_AGE+AGE_MARGIN); } } @Override public synchronized void clear() { cache.clear(); } /** * Iterates the cache removing all expired entries. * @return The age of the oldest non-expired entry in the cache or -1 if the cache is empty. */ private synchronized long clearExpired() { debug(">>> clearExpired MAX_AGE = "+MAX_AGE); Iterator<java.util.Map.Entry<K, LimitedTimeCache<K, V>.Entry>> iter = cache.entrySet().iterator(); long oldest = -1; while (iter.hasNext()) { java.util.Map.Entry<K, LimitedTimeCache<K, V>.Entry> me = iter.next(); Entry e = me.getValue(); long age = e.age(); if (age>=MAX_AGE) { debug("Expired: "+me.getKey() +" age: "+e.age()); iter.remove(); } else { oldest = Math.max(oldest, age); } } debug("<<< clearExpired ["+cache.size()+"]"); return oldest; } /** * Applies a {@link LimitedTimeCache} cache around a given function. * * @param duration time after which entries in the cache should expire. * @param func Function to wrap the cache around * @return Equivalent function but with caching. */ public static <K,V> Function<K,V> applyOn(Duration duration, Function<K,V> func) { LimitedTimeCache<K, V> cache = new LimitedTimeCache<>(duration); return (k) -> { V v = cache.get(k); if (v==null) { cache.put(k, v=func.apply(k)); } return v; }; } }