/** * Licensed to Apereo under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Apereo licenses this file to you 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 the following location: * * 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.apereo.portal.portlets.dynamicskin.storage.s3; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import javax.portlet.PortletPreferences; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonServiceException; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.GetObjectMetadataRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import net.sf.ehcache.Cache; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apereo.portal.portlets.dynamicskin.DynamicSkinException; import org.apereo.portal.portlets.dynamicskin.DynamicSkinInstanceData; import org.apereo.portal.portlets.dynamicskin.DynamicSkinUniqueTokenGenerator; import org.apereo.portal.portlets.dynamicskin.storage.AbstractDynamicSkinService; import org.apereo.portal.portlets.dynamicskin.storage.DynamicSkinCssFileNamer; import org.apereo.portal.portlets.dynamicskin.storage.DynamicSkinService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.util.Assert; /** * {@link DynamicSkinService} implementation that saves the CSS to an AWS S3 bucket. */ @Service("awsS3DynamicSkinService") public class AwsS3DynamicSkinService extends AbstractDynamicSkinService { private static final String ATTEMPTING_TO_GET_FILE_METADATA_FROM_AWS_S3_LOG_MSG = "Attempting to get file metadata from AWS S3: bucket[{}]; key[{}]"; private static final String FILE_METADATA_RETRIEVED_FROM_AWS_S3_LOG_MSG = "File metadata retrieved from AWS S3 with no reported errors: bucket[{}]; key[{}]"; private static final String ATTEMPTING_TO_SAVE_FILE_TO_AWS_S3_LOG_MSG = "Attempting to save file to AWS S3: bucket[{}]; key[{}]"; private static final String FILE_SAVED_TO_AWS_S3_LOG_MSG = "File saved to AWS S3 with no reported errors: bucket[{}]; key[{}]"; public static final String CONTENT_CACHE_CONTROL_PORTLET_PREF_NAME = "contentCacheControl"; public static final String SKIN_UNIQUE_TOKEN_METADATA_KEY = "dynamicSkinUniqueToken"; private final Logger log = LoggerFactory.getLogger(getClass()); private AmazonS3 amazonS3Client; private DynamicSkinAwsS3BucketConfig awsS3BucketConfig; private Map<String,String> awsObjectUserMetadata; @Autowired public AwsS3DynamicSkinService( final AmazonS3 client, final DynamicSkinAwsS3BucketConfig config, final DynamicSkinUniqueTokenGenerator uniqueTokenGenerator, final DynamicSkinCssFileNamer namer, @Qualifier("org.apereo.portal.skinManager.failureCache") final Cache failureCache) { super(uniqueTokenGenerator, namer, failureCache); Assert.notNull(client); Assert.notNull(config); this.amazonS3Client = client; this.awsS3BucketConfig = config; log.info("DynamicSkinAwsS3BucketConfig provided: {}", config); } @Override public String getSkinCssPath(DynamicSkinInstanceData data) { final String bucketUrl = this.awsS3BucketConfig.getBucketUrl(); if (bucketUrl.endsWith("/")) { return bucketUrl + this.getCssObjectKey(data); } else { return bucketUrl + "/" + this.getCssObjectKey(data); } } private String getCssObjectKey(DynamicSkinInstanceData data) { return this.awsS3BucketConfig.getObjectKeyPrefix() + "/" + this.getSkinCssFilename(data); } @Override protected boolean supportsRetainmentOfNonCurrentCss() { return false; } @Override protected boolean innerSkinCssFileExists(DynamicSkinInstanceData data) { final String objectKey = this.getCssObjectKey(data); log.info(ATTEMPTING_TO_GET_FILE_METADATA_FROM_AWS_S3_LOG_MSG, this.awsS3BucketConfig.getBucketName(), objectKey); final ObjectMetadata metadata = this.getMetadataFromAwsS3Bucket(objectKey); log.info(FILE_METADATA_RETRIEVED_FROM_AWS_S3_LOG_MSG, this.awsS3BucketConfig.getBucketName(), objectKey); if (metadata == null) { return false; } else { final String uniqueTokenFromS3 = metadata.getUserMetaDataOf(SKIN_UNIQUE_TOKEN_METADATA_KEY); return this.getUniqueToken(data).equals(uniqueTokenFromS3); } } private ObjectMetadata getMetadataFromAwsS3Bucket(final String objectKey) { final GetObjectMetadataRequest request = new GetObjectMetadataRequest(this.awsS3BucketConfig.getBucketName(), objectKey); try { return this.amazonS3Client.getObjectMetadata(request); } catch (AmazonServiceException ase) { if (ase.getStatusCode() == 404) { return null; } this.logAmazonServiceException(ase, request); throw new DynamicSkinException("AWS S3 'get object metadata' failure for: " + request, ase); } catch (AmazonClientException ace) { this.logAmazonClientException(ace, request); throw new DynamicSkinException("AWS S3 'get object metadata' failure for: " + request, ace); } } @Override protected void moveCssFileToFinalLocation(DynamicSkinInstanceData data, File tempCssFile) { final String objectKey = this.getCssObjectKey(data); final String content = this.readFileContentAsString(tempCssFile); this.saveContentToAwsS3Bucket(objectKey, content, data); } private String readFileContentAsString(final File file) { try { return IOUtils.toString(new FileReader(file)); } catch (IOException ioe) { throw new DynamicSkinException(ioe); } } private void saveContentToAwsS3Bucket( final String objectKey, final String content, final DynamicSkinInstanceData data) { final InputStream inputStream = IOUtils.toInputStream(content); final ObjectMetadata objectMetadata = this.createObjectMetadata(content, data); final PutObjectRequest putObjectRequest = this.createPutObjectRequest(objectKey, inputStream, objectMetadata); log.info(ATTEMPTING_TO_SAVE_FILE_TO_AWS_S3_LOG_MSG, this.awsS3BucketConfig.getBucketName(), objectKey); this.saveContentToAwsS3Bucket(putObjectRequest); log.info(FILE_SAVED_TO_AWS_S3_LOG_MSG, this.awsS3BucketConfig.getBucketName(), objectKey); } private ObjectMetadata createObjectMetadata(final String content, final DynamicSkinInstanceData data) { final ObjectMetadata metadata = new ObjectMetadata(); this.addContentMetadata(metadata, content); this.addUserMetatadata(metadata); this.addPortletPreferenceMetadata(metadata, data.getPortletRequest().getPreferences()); this.addDynamicSkinMetadata(metadata, data); return metadata; } private PutObjectRequest createPutObjectRequest( final String objectKey, final InputStream inputStream, final ObjectMetadata objectMetadata) { return new PutObjectRequest(this.awsS3BucketConfig.getBucketName(), objectKey, inputStream, objectMetadata); } private void saveContentToAwsS3Bucket(final PutObjectRequest putObjectRequest) { try { this.amazonS3Client.putObject(putObjectRequest); } catch (AmazonServiceException ase) { this.logAmazonServiceException(ase, putObjectRequest); throw new DynamicSkinException("AWS S3 'put object' failure for: " + putObjectRequest, ase); } catch (AmazonClientException ace) { this.logAmazonClientException(ace, putObjectRequest); throw new DynamicSkinException("AWS S3 'put object' failure for: " + putObjectRequest, ace); } } private void addContentMetadata(final ObjectMetadata metadata, final String content) { metadata.setContentMD5(this.calculateBase64EncodedMd5Digest(content)); metadata.setContentLength(content.length()); metadata.setContentType("text/css"); final String cacheControl = this.awsS3BucketConfig.getObjectCacheControl(); if (StringUtils.isNotEmpty(cacheControl)) { metadata.setCacheControl(cacheControl); } } private String calculateBase64EncodedMd5Digest(final String content) { final byte[] md5DigestAs16ElementByteArray = DigestUtils.md5(content); return new String(Base64.encodeBase64(md5DigestAs16ElementByteArray)); } private void addUserMetatadata(final ObjectMetadata metadata) { if (this.awsObjectUserMetadata != null) { for (Entry<String, String> entry : this.awsObjectUserMetadata.entrySet()) { metadata.addUserMetadata(entry.getKey(), entry.getValue()); } } } private void addPortletPreferenceMetadata( final ObjectMetadata metadata, final PortletPreferences portletPreferences) { final String contentCacheControl = portletPreferences.getValue(CONTENT_CACHE_CONTROL_PORTLET_PREF_NAME, null); if (contentCacheControl != null) { metadata.setCacheControl(contentCacheControl); } } private void addDynamicSkinMetadata(final ObjectMetadata metadata, final DynamicSkinInstanceData data) { metadata.addUserMetadata(SKIN_UNIQUE_TOKEN_METADATA_KEY, this.getUniqueToken(data)); } private void logAmazonClientException(final AmazonClientException exception, final AmazonWebServiceRequest request) { log.info("Caught an AmazonClientException, which means the client encountered a serious internal problem " + "while trying to communicate with S3, such as not being able to access the network."); log.info("Error Message: {}", exception.getMessage()); } private void logAmazonServiceException(final AmazonServiceException exception, final AmazonWebServiceRequest request) { log.info("Caught an AmazonServiceException, which means your request made it " + "to Amazon S3, but was rejected with an error response for some reason."); log.info("Error Message: {}", exception.getMessage()); log.info("HTTP Status Code: {}", exception.getStatusCode()); log.info("AWS Error Code: {}", exception.getErrorCode()); log.info("Error Type: {}", exception.getErrorType()); log.info("Request ID: {}", exception.getRequestId()); } public void setAwsObjectUserMetadata(final Map<String, String> metadata) { this.awsObjectUserMetadata = new HashMap<String, String>(metadata.size()); this.awsObjectUserMetadata.putAll(metadata); } }