Index: application.properties =================================================================== --- application.properties (revision 63521) +++ application.properties (working copy) @@ -1,5 +1,7 @@ -#utf-8 -#Wed May 20 09:56:00 EDT 2009 -app.grails.version=1.1.1 -plugins.code-coverage=1.1.6 -app.name=UiPerformance +#Grails Metadata file +#Thu Aug 05 16:54:55 BST 2010 +app.grails.version=1.3.3 +app.name=UiPerformance +plugins.code-coverage=1.1.6 +plugins.hibernate=1.3.3 +plugins.tomcat=1.3.3 Index: grails-app/conf/BuildConfig.groovy =================================================================== --- grails-app/conf/BuildConfig.groovy (revision 63521) +++ grails-app/conf/BuildConfig.groovy (working copy) @@ -6,3 +6,15 @@ 'UiPerformanceGrailsPlugin*', 'BuildConfig*' ] + +grails.project.dependency.resolution = { + inherits("global") { + } + log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose' + repositories { + mavenCentral() + } + dependencies { + build 'net.java.dev.jets3t:jets3t:0.7.4' + } +} \ No newline at end of file Index: grails-app/taglib/com/studentsonly/grails/plugins/uiperformance/taglib/AbstractTaglib.groovy =================================================================== --- grails-app/taglib/com/studentsonly/grails/plugins/uiperformance/taglib/AbstractTaglib.groovy (revision 63521) +++ grails-app/taglib/com/studentsonly/grails/plugins/uiperformance/taglib/AbstractTaglib.groovy (working copy) @@ -1,5 +1,9 @@ package com.studentsonly.grails.plugins.uiperformance.taglib +import com.studentsonly.grails.plugins.uiperformance.Utils +import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH + + /** * Abstract base class for taglibs. * @@ -28,20 +32,32 @@ } protected String generateRelativePath(dir, name, extension, plugin, absolute) { - if ('true' == absolute) { + + StringBuilder path = new StringBuilder() + + if ( CH.config.uiperformance.cdn.enabled ){ + + path.append Utils.getCdnPath() + + } else { + + if ('true' == absolute) { return name - } + } - String baseUri = grailsAttributes.getApplicationUri(request) - StringBuilder path = new StringBuilder(baseUri) - if (!baseUri.endsWith('/')) { - path.append '/' - } - String requestPluginContext = plugin ? pluginContextPath : '' - if (requestPluginContext) { - path.append (requestPluginContext.startsWith('/') ? requestPluginContext.substring(1) : requestPluginContext) - path.append '/' - } + String baseUri = grailsAttributes.getApplicationUri(request) + path.append( baseUri ) + if (!baseUri.endsWith('/')) { + path.append '/' + } + String requestPluginContext = plugin ? pluginContextPath : '' + if (requestPluginContext) { + path.append (requestPluginContext.startsWith('/') ? requestPluginContext.substring(1) : requestPluginContext) + path.append '/' + } + + } + if (dir) { path.append dir path.append '/' Index: plugin.xml =================================================================== --- plugin.xml (revision 63521) +++ plugin.xml (working copy) @@ -1,17 +1,19 @@ - + Burt Beckwith burt@burtbeckwith.com Grails UI Performance Plugin Taglibs and Filter to implement some of the Yahoo performance team's 14 rules http://grails.org/plugin/ui-performance + BuildConfig + UrlMappings + com.studentsonly.grails.plugins.uiperformance.taglib.AbstractTaglib + com.studentsonly.grails.plugins.uiperformance.taglib.CssTagLib + com.studentsonly.grails.plugins.uiperformance.taglib.DependantJavascriptTagLib + com.studentsonly.grails.plugins.uiperformance.taglib.ImageTagLib com.studentsonly.grails.plugins.uiperformance.taglib.JavascriptTagLib - com.studentsonly.grails.plugins.uiperformance.taglib.ImageTagLib com.studentsonly.grails.plugins.uiperformance.taglib.SpriteTagLib - com.studentsonly.grails.plugins.uiperformance.taglib.CssTagLib - com.studentsonly.grails.plugins.uiperformance.taglib.AbstractTaglib - com.studentsonly.grails.plugins.uiperformance.taglib.DependantJavascriptTagLib - BuildConfig + \ No newline at end of file Index: scripts/DeployToCdn.groovy =================================================================== --- scripts/DeployToCdn.groovy (revision 0) +++ scripts/DeployToCdn.groovy (revision 0) @@ -0,0 +1,192 @@ +import grails.util.BuildSettings + +import org.jets3t.service.S3Service +import org.jets3t.service.security.AWSCredentials +import org.jets3t.service.impl.rest.httpclient.RestS3Service +import org.jets3t.service.model.S3Bucket +import org.jets3t.service.model.S3Object +import org.jets3t.service.multithread.S3ServiceSimpleMulti +import org.jets3t.service.acl.GroupGrantee +import org.jets3t.service.acl.Permission +import org.jets3t.service.acl.AccessControlList +import org.jets3t.service.CloudFrontService +import org.jets3t.service.model.cloudfront.Distribution + +import org.apache.commons.io.FilenameUtils + +import groovy.time.* + +includeTargets << grailsScript("Init") +includeTargets << grailsScript("_GrailsCompile") +grailsHome = Ant.project.properties."environment.GRAILS_HOME" +includeTargets << new File("${grailsHome}/scripts/Bootstrap.groovy") + +target(main: "The description of the script goes here!") { + depends(bootstrap) + + // compile to make classes available + compile() + + // set up classes needed to version resources + def classLoader = Thread.currentThread().contextClassLoader + classLoader.addURL(new File(classesDirPath).toURL()) + classLoader.addURL(new File(pluginClassesDirPath).toURL()) + String className = 'com.studentsonly.grails.plugins.uiperformance.ResourceVersionHelper' + def helper = Class.forName(className, true, classLoader).newInstance() + + // copy resources to temp dir + File tempDir = File.createTempFile("uiPerformance", '') + tempDir.delete() + tempDir.mkdirs() + ant.copy(todir: tempDir) { + fileset(dir: new File(basedir, "web-app")) { + include(name: "**/*") + } + } + + println "${tempDir.absolutePath}${File.separator}WEB-INF" + // need to make a WEB-INF/classes directory or the helper will fail when trying to create the uiperformance.properties file + File classesDir = new File("${tempDir.absolutePath}${File.separator}WEB-INF${File.separator}classes") + classesDir.mkdirs() + + // version resources + helper.version tempDir, new File(basedir) + + def version = helper.determineVersion(basedir) + + // upload to s3 + S3Helper.deployToS3(config.uiperformance.cdn, tempDir, version) + +} + +setDefaultTarget(main) + +// helper class for Amazon s3 functionality +private class S3Helper { + + // keeps track of all uploaded objects + private static def filesToUpload = [] + + public static def deployToS3(cdnConfig, deployDir, version) { + if (cdnConfig.s3Enabled) { + + println "uiPerformance : starting to deploy to Amazon CDN" + + String awsAccessKey = cdnConfig.s3AccessKey + String awsSecretKey = cdnConfig.s3SecretKey + String bucketName = cdnConfig.s3BucketName + + AWSCredentials awsCredentials = new AWSCredentials(awsAccessKey, awsSecretKey) + S3Service s3Service = new RestS3Service(awsCredentials) + S3Bucket bucket = s3Service.getOrCreateBucket(bucketName) + S3ServiceSimpleMulti simpleMulti = new S3ServiceSimpleMulti(s3Service); + + // make the bucket publicly accessible + AccessControlList bucketAcl = s3Service.getBucketAcl(bucket); + bucketAcl.grantPermission(GroupGrantee.ALL_USERS, Permission.PERMISSION_READ); + bucket.setAcl(bucketAcl); + s3Service.putBucketAcl(bucket); + + new File(deployDir.absolutePath).eachFile { topDir -> + if (topDir.name == 'js' || topDir.name == 'css' || topDir.name == 'images') { + addToBucket(topDir, deployDir, version, bucketAcl) + } + } + + println "uiPerformance : uploading ${filesToUpload.size } files" + + simpleMulti.putObjects(bucket, filesToUpload as S3Object[]) + + println "uiPerformance : deployment to Amazon CDN finished" + + if (cdnConfig.cloudFrontEnabled) { + println "uiPerformance : checking CloudFront distribution" + + CloudFrontService cloudFrontService = new CloudFrontService(awsCredentials); + Distribution[] bucketDistributions = cloudFrontService.listDistributions(bucketName); + + def distributionExists = false + def cloudFrontCName = cdnConfig.cloudFrontCName as String + + for (int i = 0; i < bucketDistributions.length; i++) { + (bucketDistributions[i].CNAMEs).each { cname -> + if (cname == cloudFrontCName ) { + println "uiPerformance : CloudFront distribution found - domain name is " + println "${ bucketDistributions[i].domainName }" + println "uiPerformance : CloudFront deployment done" + distributionExists = true + } + } + } + + if (!distributionExists) { + println "uiPerformance : CloudFront distribution does not exist for CNAME '${ cloudFrontCName }', creating" + + Distribution newDistribution = cloudFrontService.createDistribution( + bucketName, + "" + System.currentTimeMillis(), // Caller reference - a unique string value + [cloudFrontCName] as String[], // CNAME aliases for distribution + "Grails Ui Performance Assets", // Comment + true, // Distribution is enabled? + null // Logging status of distribution (null means disabled) + ) + + println "uiPerformance : CloudFront distribution created - domain name is" + println "${ bucketDistributions[i].domainName }" + println "uiPerformance : CloudFront deployment done" + + } + } + + } + } + + public static def addToBucket(file, directoryRoot, version, accessControl) { + + def now = new Date() + + if (file.isDirectory()) { + file.eachFile { childFile -> + addToBucket(childFile, directoryRoot, version, accessControl) + } + } else { + def path = file.absolutePath - (directoryRoot.absolutePath + File.separator) + path = path.replaceAll('\\\\', '/') + S3Object fileObject = new S3Object(file) + fileObject.setKey(version + '/' + path) + fileObject.setAcl(accessControl) + + // set metadata + fileObject.setLastModifiedDate(now) + + use( [org.codehaus.groovy.runtime.TimeCategory] ){ + fileObject.addMetadata( "Expires", now + 10.years ) + } + + switch (FilenameUtils.getExtension(file.name).toLowerCase()) { + case 'jpg': + fileObject.setContentType("image/jpeg") + break; + case 'png': + fileObject.setContentType("image/png") + break; + case 'gif': + fileObject.setContentType("image/gif") + break; + case 'css': + fileObject.setContentType("text/css") + break; + case 'js': + fileObject.setContentType("text/javascript") + break; + } + + if (file.name.contains(".gz.")) { + fileObject.setContentEncoding("gzip") + } + + filesToUpload.add(fileObject) + } + } + +} \ No newline at end of file Index: src/groovy/com/studentsonly/grails/plugins/uiperformance/ResourceVersionHelper.groovy =================================================================== --- src/groovy/com/studentsonly/grails/plugins/uiperformance/ResourceVersionHelper.groovy (revision 63521) +++ src/groovy/com/studentsonly/grails/plugins/uiperformance/ResourceVersionHelper.groovy (working copy) @@ -55,7 +55,7 @@ } } - private String determineVersion(String basedir) { + public String determineVersion(String basedir) { def determineVersionClosure = CH.config.uiperformance.determineVersion if (determineVersionClosure instanceof Closure) { Index: src/groovy/com/studentsonly/grails/plugins/uiperformance/Utils.groovy =================================================================== --- src/groovy/com/studentsonly/grails/plugins/uiperformance/Utils.groovy (revision 63521) +++ src/groovy/com/studentsonly/grails/plugins/uiperformance/Utils.groovy (working copy) @@ -24,6 +24,7 @@ private static String _applicationVersion private static List _exclusions + private static String _cdnPath static final List DEFAULT_IMAGE_EXTENSIONS = ['gif', 'jpg', 'png', 'ico'] @@ -66,14 +67,20 @@ } int index = url.lastIndexOf('.') - return url.substring(0, index) + '__v' + _applicationVersion + url.substring(index) + return url.substring(0, index) + '__v' + getApplicationVersion() + url.substring(index) } /** * Get the current application version. */ static String getApplicationVersion() { - return isEnabled() ? _applicationVersion : '' + + if( _applicationVersion == null ){ + String basePath = new File('').absolutePath + _applicationVersion = new ResourceVersionHelper().determineVersion( basePath ) + } + + return isEnabled() ? _applicationVersion : '' } /** @@ -156,16 +163,45 @@ } static boolean getConfigBoolean(String name, boolean defaultIfMissing = true) { - def value = CH.config.uiperformance[name] + def value = CH.config.uiperformance[name] return value instanceof Boolean ? value : defaultIfMissing } static def getConfigValue(String name, defaultIfMissing = null) { - def value = CH.config.uiperformance[name] - return value ?: defaultIfMissing + def value = CH.config.uiperformance[name] + return value ?: defaultIfMissing } static boolean isEnabled() { return getConfigBoolean('enabled') } + + static String getCdnPath() { + if( !_cdnPath ){ + def ui = CH.config.uiperformance + def cdn = CH.config.uiperformance.cdn + + if (cdn.enabled) { + if (cdn.s3Enabled || cdn.cloudFrontEnabled ) { + if ( cdn.cloudFrontEnabled ) { + _cdnPath = "http:////${ cdn.cloudFrontCName ?: 'ERROR : uiperformance.cdn.cloudFrontCName not set' }/" + } else { + _cdnPath = "http:////${ cdn.s3BucketName ?: 'ERROR : uiperformance.cdn.s3.bucketname not set'}.${ cdn.s3Domain ?: 'ERROR : uiperformance.cdn.s3.domain not set' }/" + } + _cdnPath = "${ _cdnPath }${getApplicationVersion() }/" + } else { + _cdnPath = cdn.location ?: 'ERROR : uiperformance.cdn.location not set' + } + if (!_cdnPath.endsWith('/')) { + _cdnPath += '/' + } + } + } + return _cdnPath + } + + static void setCdnPath( String cdnPath ){ + _cdnPath = null + } + } Index: test/unit/com/studentsonly/grails/plugins/uiperformance/UtilsTests.groovy =================================================================== --- test/unit/com/studentsonly/grails/plugins/uiperformance/UtilsTests.groovy (revision 63521) +++ test/unit/com/studentsonly/grails/plugins/uiperformance/UtilsTests.groovy (working copy) @@ -21,4 +21,23 @@ assertEquals '/foo/bar/images/theurl__v123.gif', Utils.addVersion('/foo/bar/images/theurl.gif') } + + void testGetCdnPath() { + Utils.setCdnPath( null ) + CH.config = [ uiperformance: [enabled: true, cdn: [ enabled: true, location: "http:////www.foo.bar" ] ] ] + assertEquals 'http:////www.foo.bar/', Utils.getCdnPath() + } + + void testGetCdnPathS3() { + Utils.setCdnPath( null ) + CH.config = [uiperformance: [enabled:true, cdn: [enabled:true, s3Enabled:true, s3BucketName: 'subbucket.bucket', s3Domain: 's3.amazonaws.com' ] ] ] + assertEquals 'http:////subbucket.bucket.s3.amazonaws.com/123/', Utils.getCdnPath() + } + + void testGetCdnPathCloudFront() { + Utils.setCdnPath( null ) + CH.config = [uiperformance: [enabled:true, cdn: [enabled:true, s3Enabled:true, s3BucketName: 'subbucket.bucket', s3Domain: 's3.amazonaws.com', cloudFrontEnabled:true, cloudFrontCName: 'cloudfront.myhome.com' ] ] ] + assertEquals 'http:////cloudfront.myhome.com/123/', Utils.getCdnPath() + } + }