/* * Copyright 2008-2014 the original author or authors * * 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.kaleidofoundry.core.store; import static org.kaleidofoundry.core.i18n.InternalBundleHelper.StoreMessageBundle; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.BaseUri; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.CacheManagerRef; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.Caching; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.ConnectTimeout; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.MaxRetryOnFailure; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.ReadTimeout; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.Readonly; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.SleepTimeBeforeRetryOnFailure; import static org.kaleidofoundry.core.store.FileStoreContextBuilder.UseCaches; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLConnection; import java.util.concurrent.ConcurrentHashMap; import org.kaleidofoundry.core.cache.Cache; import org.kaleidofoundry.core.cache.CacheManager; import org.kaleidofoundry.core.cache.CacheManagerFactory; import org.kaleidofoundry.core.context.EmptyContextParameterException; import org.kaleidofoundry.core.context.RuntimeContext; import org.kaleidofoundry.core.io.FileHelper; import org.kaleidofoundry.core.io.MimeTypeResolverFactory; import org.kaleidofoundry.core.lang.annotation.Immutable; import org.kaleidofoundry.core.lang.annotation.NotNull; import org.kaleidofoundry.core.plugin.Declare; import org.kaleidofoundry.core.util.StringHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * {@link FileStore} abstract class (common to all store class implementation)<br/> * <br/> * You can create your own store, by extending this class and implement : * <ul> * <li>{@link #getStoreType()}</li> * <li>{@link #doGet(URI)}</li> * <li>{@link #doRemove(URI)}</li> * <li>{@link #doStore(URI, ResourceHandler)}</li> * </ul> * Then, annotate {@link Declare} your new class to register your implementation * * @author jraduget */ @Immutable public abstract class AbstractFileStore implements FileStore { /** default fileStore logger */ static final Logger LOGGER = LoggerFactory.getLogger(FileStore.class); protected final RuntimeContext<FileStore> context; protected final String baseUri; protected final ConcurrentHashMap<String, ResourceHandler> openedResources; protected final Cache<String, ResourceHandler> resourcesByUri; /** * runtime context injection by constructor<br/> * the file store will be registered in {@link FileStoreFactory#getRegistry()} * * @param context * @see FileStoreRegistry */ public AbstractFileStore(@NotNull final RuntimeContext<FileStore> context) { this(null, context); } /** * runtime context injection by constructor<br/> * the file store will be registered in {@link FileStoreFactory#getRegistry()} * * @param baseUri * @param context * @see FileStoreRegistry */ public AbstractFileStore(String baseUri, @NotNull final RuntimeContext<FileStore> context) { // base uri parameter baseUri = FileStoreProvider.buildFullResourceURi(!StringHelper.isEmpty(baseUri) ? baseUri : context.getString(BaseUri)); // context check if (StringHelper.isEmpty(baseUri)) { throw new EmptyContextParameterException(BaseUri, context); } this.context = context; this.baseUri = baseUri; openedResources = new ConcurrentHashMap<String, ResourceHandler>(); // internal resource cache if (context.getBoolean(Caching, false)) { final String cacheName; final CacheManager cacheManager; final String cacheManagerContextRef = context.getString(CacheManagerRef); if (!StringHelper.isEmpty(cacheManagerContextRef)) { cacheManager = CacheManagerFactory.provides(new RuntimeContext<CacheManager>(cacheManagerContextRef, CacheManager.class, context)); } else { cacheManager = CacheManagerFactory.provides(); } cacheName = "kaleidofoundry/store/" + (!StringHelper.isEmpty(context.getName()) ? context.getName() : getBaseUri().replaceAll(":", "")); resourcesByUri = cacheManager.getCache(cacheName); } else { resourcesByUri = null; } // register the named file store instance if (context.getName() != null) { FileStoreFactory.getRegistry().put(context.getName(), this); } } /* * don't use it, * this constructor is only needed and used by some IOC framework like spring. */ AbstractFileStore() { context = null; baseUri = null; resourcesByUri = null; openedResources = new ConcurrentHashMap<String, ResourceHandler>(); } /** * @return runtime context of the instance */ @NotNull protected RuntimeContext<FileStore> getContext() { return context; } /** * @return types of the store (classpath:/, file:/, http://, https://, ftp://, sftp:/...) */ @NotNull public abstract FileStoreType[] getStoreType(); /** * resource connection processing, you don't have to check argument validity * * @param resourceUri * @return resource handler * @throws ResourceException * @throws ResourceNotFoundException */ protected abstract ResourceHandler doGet(@NotNull URI resourceUri) throws ResourceNotFoundException, ResourceException; /** * remove processing, you don't have to check argument validity * * @param resourceUri * @throws ResourceException * @throws ResourceNotFoundException */ protected abstract void doRemove(@NotNull URI resourceUri) throws ResourceNotFoundException, ResourceException; /** * store processing, you don't have to check argument validity * * @param resourceUri * @param resource * @throws ResourceException * @throws ResourceNotFoundException */ protected abstract void doStore(@NotNull URI resourceUri, @NotNull ResourceHandler resource) throws ResourceException; /** * build a full resource uri, given a relative path * * @param resourceRelativePath * @return full resource uri, given the relative path parameter */ protected String buildResourceURi(final String resourceRelativePath) { boolean appendBaseUri = false; final String baseUri = getBaseUri(); final String relativePath = resourceRelativePath; final StringBuilder resourceUri = new StringBuilder(); if (relativePath != null && !relativePath.startsWith(baseUri)) { appendBaseUri = true; resourceUri.append(baseUri); } else { resourceUri.append(relativePath); } // merge variables that could be contains in the resource path final StringBuilder mergedResourceUri = new StringBuilder(FileStoreProvider.buildFullResourceURi(resourceUri.toString())); // remove '/' is baseUri ends with '/' and relativePath starts with a '/' if (appendBaseUri && baseUri != null && baseUri.endsWith("/") && relativePath != null && relativePath.startsWith("/")) { mergedResourceUri.deleteCharAt(mergedResourceUri.length() - 1); } else { // add '/' if needed if (appendBaseUri && baseUri != null && !baseUri.endsWith("/") && relativePath != null && !relativePath.startsWith("/")) { mergedResourceUri.append("/"); } } if (appendBaseUri) { mergedResourceUri.append(relativePath); } String result = mergedResourceUri.toString(); // normalize uri by using '/' as path separator result = FileHelper.buildCustomPath(result, FileHelper.UNIX_SEPARATOR, false); // normalize uri by replacing spaces by %20 result = StringHelper.replaceAll(result, " ", "%20"); return result; } @Override public ResourceHandler createResourceHandler(final String resourceUri, final InputStream input) { ResourceHandler resource = new ResourceHandlerBean(this, resourceUri, input); openedResources.put(resourceUri, resource); return resource; } @Override public ResourceHandler createResourceHandler(final String resourceUri, final String content) { ResourceHandler resource = new ResourceHandlerBean(this, resourceUri, content); return resource; } @Override public ResourceHandler createResourceHandler(final String resourceUri, final String content, final String charset) { ResourceHandler resource = new ResourceHandlerBean(this, resourceUri, content, charset); return resource; } @Override public ResourceHandler createResourceHandler(final String resourceUri, final Reader reader, final String charset) { ResourceHandler resource = new ResourceHandlerBean(this, resourceUri, reader, charset); openedResources.put(resourceUri, resource); return resource; } @Override public ResourceHandler createResourceHandler(final String resourceUri, final byte[] content) { ResourceHandler resource = new ResourceHandlerBean(this, resourceUri, content); return resource; } /** * creates a new resource handler for caching purposes.<br/> * the input and output resource handler, will sharing the same bytes data,<br/> * but the new one will have a dedicated inputStream / reader to avoid thread share problem between users * * @param resourceHandler * @return * @throws ResourceException */ protected ResourceHandler createCacheableResourceHandler(final ResourceHandler resourceHandler) throws ResourceException { ResourceHandlerBean cacheableResource = new ResourceHandlerBean(this, resourceHandler.getUri(), resourceHandler.getBytes()); cacheableResource.setLastModified(resourceHandler.getLastModified()); cacheableResource.setMimeType(resourceHandler.getMimeType()); cacheableResource.setCharset(resourceHandler.getCharset()); cacheableResource.setInputStream(new ByteArrayInputStream(resourceHandler.getBytes())); return cacheableResource; } @Override public FileStore closeAll() { for (ResourceHandler resourceHandler : openedResources.values()) { resourceHandler.close(); } return this; } @Override public void destroy() { if (context.getName() != null) { FileStoreFactory.getRegistry().remove(context.getName()); } closeAll(); } /** * opened resource cleanup * * @param resource * @see ResourceHandler#close() */ void unregisterOpenedResource(final ResourceHandler resource) { unregisterOpenedResource(resource.getUri()); } /** * opened resource cleanup * * @param resourceUri * @see ResourceHandler#close() */ void unregisterOpenedResource(final String resourceUri) { openedResources.remove(resourceUri); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#getBaseUri() */ @Override @NotNull public String getBaseUri() { return baseUri; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#isUriManageable(java.net.String) */ @Override public boolean isUriManageable(@NotNull final String pResourceUri) { final String resourceUri = buildResourceURi(pResourceUri); final FileStoreType resourceType = FileStoreTypeEnum.match(resourceUri); if (resourceType != null) { for (final FileStoreType t : getStoreType()) { if (t.equals(resourceType)) { return true; } } } throw new IllegalArgumentException(StoreMessageBundle.getMessage("store.uri.illegal", pResourceUri, getClass().getName())); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#isReadOnly() */ @Override public boolean isReadOnly() { if (StringHelper.isEmpty(context.getString(Readonly))) { return false; } else { return context.getBoolean(Readonly); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#get(java.lang.String) */ @Override public final ResourceHandler get(@NotNull final String resourceRelativePath) throws ResourceException { final String resourceUri = buildResourceURi(resourceRelativePath); isUriManageable(resourceUri); int retryCount = 0; int maxRetryCount = 1; ResourceException lastError = null; // get from cache if enabled if (resourcesByUri != null && resourcesByUri.containsKey(resourceUri)) { // create a new resourceHandler sharing same bytes data, but with a specific user inputStream / reader return createCacheableResourceHandler(resourcesByUri.get(resourceUri)); } while (retryCount < maxRetryCount) { try { // try to get the resource final ResourceHandler in = doGet(URI.create(resourceUri)); if (in == null || in.isEmpty()) { throw new ResourceNotFoundException(resourceRelativePath); } // some extra informations if (in instanceof ResourceHandlerBean) { ((ResourceHandlerBean) in).setLastModified(in.getLastModified()); ((ResourceHandlerBean) in).setMimeType(in.getMimeType() != null ? in.getMimeType() : MimeTypeResolverFactory.getService().getMimeType(FileHelper.getFileNameExtension(resourceUri))); ((ResourceHandlerBean) in).setCharset(in.getCharset()); } // if caching is enabled : put to cache if (resourcesByUri != null) { final ResourceHandler cacheableResource = createResourceHandler(resourceUri, in.getBytes()); resourcesByUri.put(resourceUri, cacheableResource); return cacheableResource; } // no cache, direct resource access else { return in; } } catch (final ResourceException rse) { lastError = rse; maxRetryCount = getMaxRetryOnFailure(); // no fail-over, we throw the exception if (maxRetryCount <= 0) { throw rse; } // wait for the configuring delay (in milliseconds) else { retryCount++; final int sleepTime = getSleepTimeBeforeRetryOnFailure(); if (retryCount < maxRetryCount) { LOGGER.warn(StoreMessageBundle.getMessage("store.failover.retry.get.info", resourceRelativePath, sleepTime, retryCount, maxRetryCount)); try { Thread.sleep((sleepTime)); } catch (final InterruptedException e) { LOGGER.error(StoreMessageBundle.getMessage("store.failover.retry.error", sleepTime), rse); throw rse; } } else { LOGGER.error(StoreMessageBundle.getMessage("store.failover.retry.get.info", resourceRelativePath, sleepTime, retryCount, maxRetryCount), rse); } } } } if (lastError != null) { throw lastError; } else { throw new IllegalStateException(); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#remove(java.lang.String) */ @Override public final FileStore remove(@NotNull final String resourceRelativePath) throws ResourceException { if (isReadOnly()) { throw new ResourceException("store.readonly.illegal", context.getName() != null ? context.getName() : ""); } final String resourceUri = buildResourceURi(resourceRelativePath); isUriManageable(resourceUri); int retryCount = 0; int maxRetryCount = 1; ResourceException lastError = null; // invalidate cache entry if (resourcesByUri != null && resourcesByUri.containsKey(resourceUri)) { resourcesByUri.remove(resourceUri); } while (retryCount < maxRetryCount) { try { // try to remove the resource doRemove(URI.create(resourceUri)); return this; } catch (final ResourceException rse) { lastError = rse; maxRetryCount = getMaxRetryOnFailure(); // no fail-over, we throw the exception if (maxRetryCount <= 0) { throw rse; } // wait for the configuring delay (in milliseconds) else { retryCount++; final int sleepTime = getSleepTimeBeforeRetryOnFailure(); if (retryCount < maxRetryCount) { LOGGER.warn(StoreMessageBundle.getMessage("store.failover.retry.remove.info", resourceRelativePath, sleepTime, retryCount, maxRetryCount)); try { Thread.sleep((sleepTime)); } catch (final InterruptedException e) { LOGGER.error(StoreMessageBundle.getMessage("store.failover.retry.error", sleepTime), rse); throw rse; } } else { LOGGER.error(StoreMessageBundle.getMessage("store.failover.retry.remove.info", resourceRelativePath, sleepTime, retryCount, maxRetryCount), rse); } } } } if (lastError != null) { throw lastError; } else { throw new IllegalStateException(); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#store(org.kaleidofoundry.core.store.ResourceHandler) */ @Override public final FileStore store(@NotNull final ResourceHandler resource) throws ResourceException { if (isReadOnly()) { throw new ResourceException("store.readonly.illegal", context.getName() != null ? context.getName() : ""); } final String resourceUri = buildResourceURi(resource.getUri()); isUriManageable(resourceUri); int retryCount = 0; int maxRetryCount = 1; ResourceException lastError = null; // invalidate cache entry if (resourcesByUri != null && resourcesByUri.containsKey(resourceUri)) { resourcesByUri.remove(resourceUri); } while (retryCount < maxRetryCount) { try { // Set some meta datas if (resource instanceof ResourceHandlerBean) { ((ResourceHandlerBean) resource).setLastModified(System.currentTimeMillis()); ((ResourceHandlerBean) resource).setMimeType(MimeTypeResolverFactory.getService().getMimeType(FileHelper.getFileNameExtension(resourceUri))); } // try to store the resource doStore(URI.create(resourceUri), resource); return this; } catch (final ResourceException rse) { lastError = rse; maxRetryCount = getMaxRetryOnFailure(); // no fail-over, we throw the exception if (maxRetryCount <= 0) { throw rse; } // wait for the configuring delay (in milliseconds) else { retryCount++; final int sleepTime = getSleepTimeBeforeRetryOnFailure(); if (retryCount < maxRetryCount) { LOGGER.warn(StoreMessageBundle.getMessage("store.failover.retry.store.info", resource.getUri(), sleepTime, retryCount, maxRetryCount)); try { Thread.sleep((sleepTime)); } catch (final InterruptedException e) { LOGGER.error(StoreMessageBundle.getMessage("store.failover.retry.error", sleepTime), rse); throw rse; } } else { LOGGER.error( StoreMessageBundle.getMessage("store.failover.retry.store.info", resource.getUri(), sleepTime, retryCount, maxRetryCount), rse); } } } } if (lastError != null) { throw lastError; } else { throw new IllegalStateException(); } } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#store(java.lang.String, java.io.InputStream) */ @Override public FileStore store(final String resourceRelativePath, final InputStream resourceContent) throws ResourceException { return store(createResourceHandler(resourceRelativePath, resourceContent)); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#store(java.lang.String, byte[]) */ @Override public FileStore store(final String resourceRelativePath, final byte[] resourceContent) throws ResourceException { return store(createResourceHandler(resourceRelativePath, resourceContent)); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#store(java.lang.String, java.lang.String) */ @Override public FileStore store(final String resourceRelativePath, final String resourceContent) throws ResourceException, UnsupportedEncodingException { return store(createResourceHandler(resourceRelativePath, resourceContent)); } /* * (non-Javadoc) * @see org.kaleidofoundry.core.store.FileStore#move(java.lang.String, java.lang.String) */ @Override public final FileStore move(@NotNull final String origin, @NotNull final String destination) throws ResourceNotFoundException, ResourceException { if (isReadOnly()) { throw new ResourceException("store.readonly.illegal", context.getName() != null ? context.getName() : ""); } isUriManageable(buildResourceURi(origin)); isUriManageable(buildResourceURi(destination)); if (exists(destination)) { remove(destination); } final ResourceHandler resource = get(origin); try { if (resource != null) { store(createResourceHandler(destination, resource.getInputStream())); } } finally { resource.close(); } remove(origin); return this; } /* * (non-Javadoc) * @see org.kaleidofoundry.core.lang.pattern.Store#exists(java.lang.Object) */ @Override public final boolean exists(@NotNull final String resourceRelativePath) throws ResourceException { ResourceHandler resource = null; try { resource = get(resourceRelativePath); return true; } catch (final ResourceNotFoundException rnfe) { return false; } finally { if (resource != null) { resource.close(); } } } /** * @param urlConnection */ protected void setUrlConnectionSettings(final URLConnection urlConnection) { if (!StringHelper.isEmpty(context.getString(ConnectTimeout))) { urlConnection.setConnectTimeout(context.getInteger(ConnectTimeout)); } if (!StringHelper.isEmpty(context.getString(ReadTimeout))) { urlConnection.setReadTimeout(context.getInteger(ReadTimeout)); } if (!StringHelper.isEmpty(context.getString(UseCaches))) { urlConnection.setUseCaches(context.getBoolean(UseCaches)); } } /** * @return max attempt after failure * @see FileStoreContextBuilder#MaxRetryOnFailure */ protected int getMaxRetryOnFailure() { final Integer maxRetry = context.getInteger(MaxRetryOnFailure); if (maxRetry != null) { return maxRetry.intValue(); } else { return -1; } } /** * @return time to sleep after failure * @see FileStoreContextBuilder#SleepTimeBeforeRetryOnFailure */ protected int getSleepTimeBeforeRetryOnFailure() { final Integer sleepOnFailure = context.getInteger(SleepTimeBeforeRetryOnFailure); if (sleepOnFailure != null) { return sleepOnFailure.intValue(); } else { return 0; } } }