/*
* #%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.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadleafcommerce.cms.field.type.StorageType;
import org.broadleafcommerce.cms.file.StaticAssetMultiTenantExtensionManager;
import org.broadleafcommerce.cms.file.dao.StaticAssetDao;
import org.broadleafcommerce.cms.file.domain.ImageStaticAsset;
import org.broadleafcommerce.cms.file.domain.ImageStaticAssetImpl;
import org.broadleafcommerce.cms.file.domain.StaticAsset;
import org.broadleafcommerce.cms.file.domain.StaticAssetImpl;
import org.broadleafcommerce.common.extension.ExtensionResultStatusType;
import org.broadleafcommerce.common.file.service.StaticAssetPathService;
import org.broadleafcommerce.common.util.TransactionUtils;
import org.broadleafcommerce.openadmin.server.service.artifact.image.ImageArtifactProcessor;
import org.broadleafcommerce.openadmin.server.service.artifact.image.ImageMetadata;
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.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.annotation.Resource;
import eu.medsea.mimeutil.MimeType;
import eu.medsea.mimeutil.MimeUtil;
import eu.medsea.mimeutil.detector.ExtensionMimeDetector;
import eu.medsea.mimeutil.detector.MagicMimeMimeDetector;
/**
* Created by bpolster.
*/
@Service("blStaticAssetService")
public class StaticAssetServiceImpl implements StaticAssetService {
private static final Log LOG = LogFactory.getLog(StaticAssetServiceImpl.class);
@Resource(name = "blImageArtifactProcessor")
protected ImageArtifactProcessor imageArtifactProcessor;
@Value("${asset.use.filesystem.storage}")
protected boolean storeAssetsOnFileSystem = false;
@Resource(name="blStaticAssetDao")
protected StaticAssetDao staticAssetDao;
@Resource(name="blStaticAssetStorageService")
protected StaticAssetStorageService staticAssetStorageService;
@Resource(name = "blStaticAssetPathService")
protected StaticAssetPathService staticAssetPathService;
@Resource(name = "blStaticAssetMultiTenantExtensionManager")
protected StaticAssetMultiTenantExtensionManager staticAssetExtensionManager;
private final Random random = new Random();
private final String FILE_NAME_CHARS = "0123456789abcdef";
@Override
public StaticAsset findStaticAssetById(Long id) {
return staticAssetDao.readStaticAssetById(id);
}
@Override
public List<StaticAsset> readAllStaticAssets() {
return staticAssetDao.readAllStaticAssets();
}
static {
MimeUtil.registerMimeDetector(ExtensionMimeDetector.class.getName());
MimeUtil.registerMimeDetector(MagicMimeMimeDetector.class.getName());
}
protected String getFileExtension(String fileName) {
int pos = fileName.lastIndexOf(".");
if (pos > 0) {
return fileName.substring(pos + 1, fileName.length()).toLowerCase();
} else {
LOG.warn("No extension provided for asset : " + fileName);
return null;
}
}
/**
* Generates a filename as a set of Hex digits.
* @param size
* @return
*/
protected String generateFileName(int size) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < size; i++) {
int pos = random.nextInt(FILE_NAME_CHARS.length());
sb = sb.append(FILE_NAME_CHARS.charAt(pos));
}
return sb.toString();
}
/**
* Will assemble the url from the passed in properties as
* /{entityType}/{fileName}
* /product/7001-ab12
*
* If the properties above are not set, it will generate the fileName randomly.
*
* @param url
* @param asset
* @param assetProperties
* @return
*/
protected String buildAssetURL(Map<String, String> assetProperties, String originalFilename) {
StringBuilder path = new StringBuilder("/");
String entityType = assetProperties.get("entityType");
String entityId = assetProperties.get("entityId");
String fileName = assetProperties.get("fileName");
if (entityType != null && !"null".equals(entityType)) {
path = path.append(entityType).append("/");
}
if (entityId != null && !"null".equals(entityId)) {
path = path.append(entityId).append("/");
}
if (fileName != null) {
int pos = fileName.indexOf(":");
if (pos > 0) {
if (LOG.isTraceEnabled()) {
LOG.trace("Removing protocol from URL name" + fileName);
}
fileName = fileName.substring(pos + 1);
}
} else {
fileName = originalFilename;
}
return path.append(fileName).toString();
}
@Override
@Transactional(TransactionUtils.DEFAULT_TRANSACTION_MANAGER)
public StaticAsset createStaticAssetFromFile(MultipartFile file, Map<String, String> properties) {
try {
return createStaticAsset(file.getInputStream(), file.getOriginalFilename(), file.getSize(), properties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
@Transactional(TransactionUtils.DEFAULT_TRANSACTION_MANAGER)
public StaticAsset createStaticAsset(InputStream inputStream, String fileName, long fileSize, Map<String, String> properties) {
if (properties == null) {
properties = new HashMap<String, String>();
}
String fullUrl = buildAssetURL(properties, fileName);
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(fullUrl);
ExtensionResultStatusType resultStatusType = staticAssetExtensionManager.getProxy().modifyDuplicateAssetURL(urlBuilder);
fullUrl = urlBuilder.toString();
StaticAsset newAsset = staticAssetDao.readStaticAssetByFullUrl(fullUrl);
// If no ExtensionManager modified the URL to handle duplicates, then go ahead and run default
// logic for handling duplicate files.
if(resultStatusType != ExtensionResultStatusType.HANDLED){
int count = 0;
while (newAsset != null) {
count++;
//try the new format first, then the old
newAsset = staticAssetDao.readStaticAssetByFullUrl(getCountUrl(fullUrl, count, false));
if (newAsset == null) {
newAsset = staticAssetDao.readStaticAssetByFullUrl(getCountUrl(fullUrl, count, true));
}
}
if (count > 0) {
fullUrl = getCountUrl(fullUrl, count, false);
}
}
try {
ImageMetadata metadata = imageArtifactProcessor.getImageMetadata(inputStream);
newAsset = new ImageStaticAssetImpl();
((ImageStaticAsset) newAsset).setWidth(metadata.getWidth());
((ImageStaticAsset) newAsset).setHeight(metadata.getHeight());
} catch (Exception e) {
//must not be an image stream
newAsset = new StaticAssetImpl();
}
if (storeAssetsOnFileSystem) {
newAsset.setStorageType(StorageType.FILESYSTEM);
} else {
newAsset.setStorageType(StorageType.DATABASE);
}
newAsset.setName(fileName);
getMimeType(inputStream, fileName, newAsset);
newAsset.setFileExtension(getFileExtension(fileName));
newAsset.setFileSize(fileSize);
newAsset.setFullUrl(fullUrl);
return staticAssetDao.addOrUpdateStaticAsset(newAsset, false);
}
/**
* Gets the count URL based on the original fullUrl. If requested in legacy format this will return URLs like:
*
* /path/to/image.jpg-1
* /path/to/image.jpg-2
*
* Whereas if this is in non-legacy format (<b>legacy</b> == false):
*
* /path/to/image-1.jpg
* /path/to/image-2.jpg
*
* Used to deal with duplicate URLs of uploaded assets
*
*/
protected String getCountUrl(String fullUrl, int count, boolean legacyFormat) {
String countUrl = fullUrl + '-' + count;
int dotIndex = fullUrl.lastIndexOf('.');
if (dotIndex != -1 && !legacyFormat) {
countUrl = fullUrl.substring(0, dotIndex) + '-' + count + '.' + fullUrl.substring(dotIndex + 1);
}
return countUrl;
}
protected void getMimeType(InputStream inputStream, String fileName, StaticAsset newAsset) {
Collection mimeTypes = MimeUtil.getMimeTypes(fileName);
if (!mimeTypes.isEmpty()) {
MimeType mimeType = (MimeType) mimeTypes.iterator().next();
newAsset.setMimeType(mimeType.toString());
} else {
mimeTypes = MimeUtil.getMimeTypes(inputStream);
if (!mimeTypes.isEmpty()) {
MimeType mimeType = (MimeType) mimeTypes.iterator().next();
newAsset.setMimeType(mimeType.toString());
}
}
}
@Override
public StaticAsset findStaticAssetByFullUrl(String fullUrl) {
try {
fullUrl = URLDecoder.decode(fullUrl, "UTF-8");
//strip out the jsessionid if it's there
fullUrl = fullUrl.replaceAll("(?i);jsessionid.*?=.*?(?=\\?|$)", "");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Unsupported encoding to decode fullUrl", e);
}
return staticAssetDao.readStaticAssetByFullUrl(fullUrl);
}
@Override
@Transactional(TransactionUtils.DEFAULT_TRANSACTION_MANAGER)
public StaticAsset addStaticAsset(StaticAsset staticAsset) {
StaticAsset newAsset = staticAssetDao.addOrUpdateStaticAsset(staticAsset, true);
return newAsset;
}
@Override
@Transactional(TransactionUtils.DEFAULT_TRANSACTION_MANAGER)
public StaticAsset updateStaticAsset(StaticAsset staticAsset) {
return staticAssetDao.addOrUpdateStaticAsset(staticAsset, true);
}
@Override
@Transactional(TransactionUtils.DEFAULT_TRANSACTION_MANAGER)
public void deleteStaticAsset(StaticAsset staticAsset) {
staticAssetDao.delete(staticAsset);
}
@Override
public String getStaticAssetUrlPrefix() {
return staticAssetPathService.getStaticAssetUrlPrefix();
}
@Override
public String getStaticAssetEnvironmentUrlPrefix() {
return staticAssetPathService.getStaticAssetEnvironmentUrlPrefix();
}
@Override
public String getStaticAssetEnvironmentSecureUrlPrefix() {
return staticAssetPathService.getStaticAssetEnvironmentSecureUrlPrefix();
}
@Override
public String convertAssetPath(String assetPath, String contextPath, boolean secureRequest) {
return staticAssetPathService.convertAssetPath(assetPath, contextPath, secureRequest);
}
}