/*
* #%L
* BroadleafCommerce CMS Module
* %%
* Copyright (C) 2009 - 2013 Broadleaf Commerce
* %%
* 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.
* #L%
*/
package org.broadleafcommerce.cms.file.service;
import org.apache.commons.io.FileExistsException;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.cms.common.AssetNotFoundException;
import org.broadleafcommerce.cms.field.type.StorageType;
import org.broadleafcommerce.cms.file.dao.StaticAssetStorageDao;
import org.broadleafcommerce.cms.file.domain.StaticAsset;
import org.broadleafcommerce.cms.file.domain.StaticAssetStorage;
import org.broadleafcommerce.cms.file.service.operation.NamedOperationManager;
import org.broadleafcommerce.common.extension.ExtensionResultHolder;
import org.broadleafcommerce.common.extension.ExtensionResultStatusType;
import org.broadleafcommerce.common.file.domain.FileWorkArea;
import org.broadleafcommerce.common.file.service.BroadleafFileService;
import org.broadleafcommerce.common.file.service.GloballySharedInputStream;
import org.broadleafcommerce.openadmin.server.service.artifact.ArtifactService;
import org.broadleafcommerce.openadmin.server.service.artifact.image.Operation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Blob;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Resource;
/**
* @author Jeff Fischer, Brian Polster
*/
@Service("blStaticAssetStorageService")
public class StaticAssetStorageServiceImpl implements StaticAssetStorageService {
@Value("${asset.server.max.uploadable.file.size}")
protected long maxUploadableFileSize;
@Value("${asset.server.file.buffer.size}")
protected int fileBufferSize = 8096;
private static final Log LOG = LogFactory.getLog(StaticAssetStorageServiceImpl.class);
protected String cacheDirectory;
@Resource(name="blStaticAssetService")
protected StaticAssetService staticAssetService;
@Resource(name = "blFileService")
protected BroadleafFileService broadleafFileService;
@Resource(name="blArtifactService")
protected ArtifactService artifactService;
@Resource(name="blStaticAssetStorageDao")
protected StaticAssetStorageDao staticAssetStorageDao;
@Resource(name="blNamedOperationManager")
protected NamedOperationManager namedOperationManager;
@Resource(name = "blStaticAssetServiceExtensionManager")
protected StaticAssetServiceExtensionManager extensionManager;
protected StaticAsset findStaticAsset(String fullUrl) {
StaticAsset staticAsset = staticAssetService.findStaticAssetByFullUrl(fullUrl);
return staticAsset;
}
protected boolean shouldUseSharedFile(InputStream is) {
return (is != null && is instanceof GloballySharedInputStream);
}
protected File getFileFromLocalRepository(String cachedFileName) {
// Look for a shared file (this represents a file that was based on a file originally in the classpath.
File cacheFile = null;
if (extensionManager != null) {
ExtensionResultHolder holder = new ExtensionResultHolder();
ExtensionResultStatusType result = extensionManager.getProxy().fileExists(cachedFileName, holder);
if (ExtensionResultStatusType.HANDLED.equals(result)) {
cacheFile = (File) holder.getResult();
}
}
if (cacheFile == null) {
cacheFile = broadleafFileService.getSharedLocalResource(cachedFileName);
}
if (cacheFile.exists()) {
return cacheFile;
} else {
return broadleafFileService.getLocalResource(cachedFileName);
}
}
protected File lookupAssetAndCreateLocalFile(StaticAsset staticAsset, File baseLocalFile)
throws IOException, SQLException {
if (StorageType.FILESYSTEM.equals(staticAsset.getStorageType())) {
File returnFile = broadleafFileService.getResource(staticAsset.getFullUrl());
if (!returnFile.getAbsolutePath().equals(baseLocalFile.getAbsolutePath())) {
createLocalFileFromInputStream(new FileInputStream(returnFile), baseLocalFile);
}
return broadleafFileService.getResource(staticAsset.getFullUrl());
} else {
StaticAssetStorage storage = readStaticAssetStorageByStaticAssetId(staticAsset.getId());
if (storage != null) {
InputStream is = storage.getFileData().getBinaryStream();
createLocalFileFromInputStream(is, baseLocalFile);
}
}
return baseLocalFile;
}
protected void createLocalFileFromClassPathResource(StaticAsset staticAsset, File baseLocalFile) throws IOException {
InputStream is = broadleafFileService.getClasspathResource(staticAsset.getFullUrl());
createLocalFileFromInputStream(is, baseLocalFile);
}
protected void createLocalFileFromInputStream(InputStream is, File baseLocalFile) throws IOException {
FileOutputStream tos = null;
FileWorkArea workArea = null;
try {
if (!baseLocalFile.getParentFile().exists()) {
boolean directoriesCreated = false;
if (!baseLocalFile.getParentFile().exists()) {
directoriesCreated = baseLocalFile.getParentFile().mkdirs();
if (!directoriesCreated) {
// There is a chance that another VM created the directories. If not, we may not have
// proper permissions and this is an error we need to report.
if (!baseLocalFile.getParentFile().exists()) {
throw new RuntimeException("Unable to create middle directories for file: " +
baseLocalFile.getAbsolutePath());
}
}
}
}
workArea = broadleafFileService.initializeWorkArea();
File tmpFile = new File(FilenameUtils.concat(workArea.getFilePathLocation(), baseLocalFile.getName()));
tos = new FileOutputStream(tmpFile);
IOUtils.copy(is, tos);
// close the input/output streams before trying to move files around
is.close();
tos.close();
// Adding protection against this file already existing / being written by another thread.
// Adding locks would be useless here since another VM could be executing the code.
if (!baseLocalFile.exists()) {
try {
FileUtils.moveFile(tmpFile, baseLocalFile);
} catch (FileExistsException e) {
// No problem
if (LOG.isDebugEnabled()) {
LOG.debug("File exists error moving file " + tmpFile.getAbsolutePath(), e);
}
}
}
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(tos);
if (workArea != null) {
broadleafFileService.closeWorkArea(workArea);
}
}
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public Map<String, String> getCacheFileModel(String fullUrl, Map<String, String> parameterMap) throws Exception {
StaticAsset staticAsset = findStaticAsset(fullUrl);
if (staticAsset == null) {
throw new AssetNotFoundException("Unable to find an asset for the url (" + fullUrl + ")");
}
String mimeType = staticAsset.getMimeType();
//extract the values for any named parameters
Map<String, String> convertedParameters = namedOperationManager.manageNamedParameters(parameterMap);
String cachedFileName = constructCacheFileName(staticAsset, convertedParameters);
// Look for a shared file (this represents a file that was based on a file originally in the classpath.
File cacheFile = getFileFromLocalRepository(cachedFileName);
if (cacheFile.exists()) {
return buildModel(cacheFile.getAbsolutePath(), mimeType);
}
// Obtain the base file (that we may need to convert based on the parameters
String baseCachedFileName = constructCacheFileName(staticAsset, null);
File baseLocalFile = getFileFromLocalRepository(baseCachedFileName);
if (! baseLocalFile.exists()) {
if (broadleafFileService.checkForResourceOnClassPath(staticAsset.getFullUrl())) {
cacheFile = broadleafFileService.getSharedLocalResource(cachedFileName);
baseLocalFile = broadleafFileService.getSharedLocalResource(baseCachedFileName);
createLocalFileFromClassPathResource(staticAsset, baseLocalFile);
} else {
baseLocalFile = lookupAssetAndCreateLocalFile(staticAsset, baseLocalFile);
}
}
if (convertedParameters.isEmpty()) {
return buildModel(baseLocalFile.getAbsolutePath(), mimeType);
} else {
FileInputStream assetStream = new FileInputStream(baseLocalFile);
BufferedInputStream original = new BufferedInputStream(assetStream);
original.mark(0);
Operation[] operations = artifactService.buildOperations(convertedParameters, original, staticAsset.getMimeType());
InputStream converted = artifactService.convert(original, operations, staticAsset.getMimeType());
createLocalFileFromInputStream(converted, cacheFile);
if ("image/gif".equals(mimeType)) {
mimeType = "image/png";
}
return buildModel(cacheFile.getAbsolutePath(), mimeType);
}
}
protected Map<String, String> buildModel(String returnFilePath, String mimeType) {
Map<String, String> model = new HashMap<String, String>(2);
model.put("cacheFilePath", returnFilePath);
model.put("mimeType", mimeType);
return model;
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public StaticAssetStorage findStaticAssetStorageById(Long id) {
return staticAssetStorageDao.readStaticAssetStorageById(id);
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public StaticAssetStorage create() {
return staticAssetStorageDao.create();
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public StaticAssetStorage readStaticAssetStorageByStaticAssetId(Long id) {
return staticAssetStorageDao.readStaticAssetStorageByStaticAssetId(id);
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public StaticAssetStorage save(StaticAssetStorage assetStorage) {
return staticAssetStorageDao.save(assetStorage);
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public void delete(StaticAssetStorage assetStorage) {
staticAssetStorageDao.delete(assetStorage);
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public Blob createBlob(MultipartFile uploadedFile) throws IOException {
return staticAssetStorageDao.createBlob(uploadedFile);
}
/**
* Builds a file system path for the passed in static asset and paramaterMap.
*
* @param staticAsset
* @param parameterMap
* @param useSharedFile
* @return
*/
protected String constructCacheFileName(StaticAsset staticAsset, Map<String, String> parameterMap) {
String fileName = staticAsset.getFullUrl();
StringBuilder sb = new StringBuilder(200);
sb.append(fileName.substring(0, fileName.lastIndexOf('.')));
sb.append("---");
StringBuilder sb2 = new StringBuilder(200);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
if (staticAsset.getAuditable() != null) {
sb2.append(format.format(staticAsset.getAuditable().getDateUpdated() == null ? staticAsset.getAuditable().getDateCreated() : staticAsset.getAuditable().getDateUpdated()));
}
if (parameterMap != null) {
for (Map.Entry<String, String> entry : parameterMap.entrySet()) {
sb2.append('-');
sb2.append(entry.getKey());
sb2.append('-');
sb2.append(entry.getValue());
}
}
String digest;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(sb2.toString().getBytes());
BigInteger number = new BigInteger(1,messageDigest);
digest = number.toString(16);
} catch(NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
sb.append(pad(digest, 32, '0'));
sb.append(fileName.substring(fileName.lastIndexOf('.')));
return sb.toString();
}
protected String pad(String s, int length, char pad) {
StringBuilder buffer = new StringBuilder(s);
while (buffer.length() < length) {
buffer.insert(0, pad);
}
return buffer.toString();
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public void createStaticAssetStorageFromFile(MultipartFile file, StaticAsset staticAsset) throws IOException {
createStaticAssetStorage(file.getInputStream(), staticAsset);
}
@Transactional("blTransactionManagerAssetStorageInfo")
@Override
public void createStaticAssetStorage(InputStream fileInputStream, StaticAsset staticAsset) throws IOException {
if (StorageType.DATABASE.equals(staticAsset.getStorageType())) {
StaticAssetStorage storage = staticAssetStorageDao.create();
storage.setStaticAssetId(staticAsset.getId());
Blob uploadBlob = staticAssetStorageDao.createBlob(fileInputStream, staticAsset.getFileSize());
storage.setFileData(uploadBlob);
staticAssetStorageDao.save(storage);
} else if (StorageType.FILESYSTEM.equals(staticAsset.getStorageType())) {
FileWorkArea tempWorkArea = broadleafFileService.initializeWorkArea();
// Convert the given URL from the asset to a system-specific suitable file path
String destFileName = FilenameUtils.normalize(tempWorkArea.getFilePathLocation() + File.separator + FilenameUtils.separatorsToSystem(staticAsset.getFullUrl()));
InputStream input = fileInputStream;
byte[] buffer = new byte[fileBufferSize];
File destFile = new File(destFileName);
if (!destFile.getParentFile().exists()) {
if (!destFile.getParentFile().mkdirs()) {
if (!destFile.getParentFile().exists()) {
throw new RuntimeException("Unable to create parent directories for file: " + destFileName);
}
}
}
OutputStream output = new FileOutputStream(destFile);
boolean deleteFile = false;
try {
int bytesRead;
int totalBytesRead = 0;
while ((bytesRead = input.read(buffer)) != -1) {
totalBytesRead += bytesRead;
if (totalBytesRead > maxUploadableFileSize) {
deleteFile = true;
throw new IOException("Maximum Upload File Size Exceeded");
}
output.write(buffer, 0, bytesRead);
}
// close the output file stream prior to moving files around
output.close();
broadleafFileService.addOrUpdateResource(tempWorkArea, destFile, deleteFile);
} finally {
IOUtils.closeQuietly(output);
broadleafFileService.closeWorkArea(tempWorkArea);
}
}
}
}