/** * Copyright (c) 2010, 2013 Darmstadt University of Technology. * 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: * Marcel Bruch - initial API and implementation. */ package org.eclipse.recommenders.models; import static com.google.common.base.Optional.absent; import static com.google.common.io.ByteStreams.toByteArray; import static com.google.common.io.Files.newInputStreamSupplier; import static java.util.concurrent.TimeUnit.MINUTES; import static org.eclipse.recommenders.utils.Zips.closeQuietly; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.Map.Entry; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.eclipse.recommenders.utils.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.util.concurrent.UncheckedExecutionException; /** * A non-thread-safe implementation of {@link IModelProvider} that loads models from model ZIP files using a * {@link ModelRepository}. Note that {@link #acquireModel(IUniqueName)} attempts to download matching model archives * immediately and thus blocks until the download is completed. */ public abstract class SimpleModelProvider<K extends IUniqueName<?>, M> implements IModelProvider<K, M> { private static final Logger LOG = LoggerFactory.getLogger(SimpleModelProvider.class); private static final int CACHE_SIZE = 10; private final LoadingCache<ModelCoordinate, ZipFile> openZips = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE) .expireAfterAccess(1, MINUTES).removalListener(new ZipRemovalListener()).build(new ZipCacheLoader()); private final IModelRepository repository; private final IModelArchiveCoordinateAdvisor index; private final String modelType; private final Map<String, IInputStreamTransformer> transformers; public SimpleModelProvider(IModelRepository cache, IModelArchiveCoordinateAdvisor index, String modelType, Map<String, IInputStreamTransformer> transformers) { this.repository = cache; this.index = index; this.modelType = modelType; this.transformers = transformers; } @Override public Optional<M> acquireModel(K key) { try { // unknown model? return immediately ModelCoordinate mc = index.suggest(key.getProjectCoordinate(), modelType).orNull(); if (mc == null) { return absent(); } final ZipFile zip; try { zip = openZips.get(mc); } catch (UncheckedExecutionException e) { if (IllegalStateException.class.equals(e.getCause().getClass())) { // repository.getLocation(..) returned absent. Try to load ZIP file again next time. // This is a workaround as LoadingCache cannot be made to cache hits but try again on a cache miss. return absent(); } else { throw e; } } return doAcquireModel(key, zip); } catch (Exception e) { LOG.error("Exception while loading model " + key, e); return absent(); } } @VisibleForTesting protected Optional<M> doAcquireModel(K key, ZipFile zip) throws IOException { InputStream in = null; try { String basePath = getBasePath(key); in = getInputStream(zip, basePath).orNull(); if (in == null) { return absent(); } else { return Optional.of(loadModel(in, key)); } } catch (UncheckedExecutionException e) { if (IllegalStateException.class.equals(e.getCause().getClass())) { // repository.getLocation(..) returned absent. Try to load ZIP file again next time. return absent(); } else { throw e; } } finally { IOUtils.closeQuietly(in); } } protected abstract String getBasePath(K key); @VisibleForTesting protected Optional<InputStream> getInputStream(ZipFile zip, String basePath) throws IOException { for (Entry<String, IInputStreamTransformer> transformer : transformers.entrySet()) { ZipEntry toTransform = zip.getEntry(basePath + "." + transformer.getKey()); //$NON-NLS-1$ if (toTransform != null) { return Optional.of(transformer.getValue().transform(zip.getInputStream(toTransform))); } } ZipEntry entry = zip.getEntry(basePath); if (entry == null) { return absent(); } return Optional.of(zip.getInputStream(entry)); } protected abstract M loadModel(InputStream stream, K key) throws IOException; @Override public void releaseModel(M value) { } @Override public void open() throws IOException { } @Override public void close() throws IOException { openZips.invalidateAll(); } /** * Resolves the given model archive coordinate from models store, puts the zip file into the cache, and loads the * file contents completely in memory for faster data access. */ private final class ZipCacheLoader extends CacheLoader<ModelCoordinate, ZipFile> { @Override public ZipFile load(ModelCoordinate key) throws Exception { File location = repository.getLocation(key, true).get(); // read file in memory to speed up access toByteArray(newInputStreamSupplier(location)); return new ZipFile(location); } } /** * Closes an zip file evicted from the cache. */ private final class ZipRemovalListener implements RemovalListener<ModelCoordinate, ZipFile> { @Override public void onRemoval(RemovalNotification<ModelCoordinate, ZipFile> notification) { closeQuietly(notification.getValue()); } } }