diff --git a/ResourcesGrailsPlugin.groovy b/ResourcesGrailsPlugin.groovy index c7e7d6a..a28a8d6 100644 --- a/ResourcesGrailsPlugin.groovy +++ b/ResourcesGrailsPlugin.groovy @@ -1,17 +1,16 @@ import grails.util.Environment -import org.codehaus.groovy.grails.commons.ConfigurationHolder - -import org.springframework.beans.factory.config.MethodInvokingFactoryBean -import org.springframework.core.io.FileSystemResource - -import org.grails.plugin.resource.util.HalfBakedLegacyLinkGenerator - +import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit -import java.util.concurrent.ScheduledFuture +import org.grails.plugin.resource.DevModeSanityFilter +import org.grails.plugin.resource.ProcessingFilter +import org.grails.plugin.resource.ResourceProcessor +import org.grails.plugin.resource.util.HalfBakedLegacyLinkGenerator +import org.springframework.core.io.FileSystemResource import org.springframework.util.AntPathMatcher +import org.springframework.web.filter.DelegatingFilterProxy /** * @author Marc Palmer (marc@grailsrocks.com) @@ -19,8 +18,20 @@ import org.springframework.util.AntPathMatcher */ class ResourcesGrailsPlugin { - static DEFAULT_URI_PREFIX = 'static' - static DEFAULT_ADHOC_PATTERNS = ["/images/*", "*.css", "*.js"].asImmutable() + static final String DEFAULT_URI_PREFIX = 'static' + static final List DEFAULT_ADHOC_PATTERNS = ["/images/*", "*.css", "*.js"].asImmutable() + static final int RELOAD_THROTTLE_DELAY = 500 + + private ScheduledThreadPoolExecutor delayedChangeThrottle = new ScheduledThreadPoolExecutor(1) + private ScheduledFuture reloadTask + + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher() + private static final List RELOADABLE_RESOURCE_EXCLUDES = [ + '**/.svn/**/*.*', + '**/.git/**/*.*', + 'WEB-INF/**/*.*', + 'META-INF/**/*.*' + ] def version = "1.2" def grailsVersion = "1.3 > *" @@ -47,112 +58,90 @@ class ResourcesGrailsPlugin { "file:./web-app/**/*.*" // Watch for resource changes, we need excludes here for WEB-INF+META-INF when grails impls this ] - def author = "Marc Palmer, Luke Daley" - def authorEmail = "marc@grailsrocks.com, ld@ldaley.com" def title = "Resources" def description = 'HTML resource management enhancements to replace g.resource etc.' def documentation = "http://grails-plugins.github.com/grails-resources" def license = "APACHE" - def organization = [ name: "Grails Community", url: "http://grails.org/" ] + def organization = [name: "Grails Community", url: "http://grails.org/"] def developers = [ - [ name: "Marc Palmer", email: "marc@grailsrocks.com" ], - [ name: "Luke Daley", email: "ld@ldaley.com" ] + [name: "Marc Palmer", email: "marc@grailsrocks.com"], + [name: "Luke Daley", email: "ld@ldaley.com"] ] - def issueManagement = [ system: "JIRA", url: "http://jira.grails.org/browse/GPRESOURCES" ] - def scm = [ url: "https://github.com/grails-plugins/grails-resources" ] - + def issueManagement = [system: "JIRA", url: "http://jira.grails.org/browse/GPRESOURCES"] + def scm = [url: "https://github.com/grails-plugins/grails-resources"] + def getWebXmlFilterOrder() { def FilterManager = getClass().getClassLoader().loadClass('grails.plugin.webxml.FilterManager') - [ DeclaredResourcesPluginFilter: FilterManager.DEFAULT_POSITION - 300, - AdHocResourcesPluginFilter: FilterManager.DEFAULT_POSITION - 250, - ResourcesDevModeFilter: FilterManager.RELOAD_POSITION + 100] + [DeclaredResourcesPluginFilter: FilterManager.DEFAULT_POSITION - 300, + AdHocResourcesPluginFilter: FilterManager.DEFAULT_POSITION - 250, + ResourcesDevModeFilter: FilterManager.RELOAD_POSITION + 100] } - def getResourcesConfig(application) { + private getResourcesConfig(application) { application.config.grails.resources } - - def getUriPrefix(application) { + + private getUriPrefix(application) { def prf = getResourcesConfig(application).uri.prefix prf instanceof String ? prf : DEFAULT_URI_PREFIX } - - def getAdHocPatterns(application) { + + private getAdHocPatterns(application) { def patterns = getResourcesConfig(application).adhoc.patterns patterns instanceof List ? patterns : DEFAULT_ADHOC_PATTERNS } - + def doWithSpring = { -> if (!springConfig.containsBean('grailsLinkGenerator')) { grailsLinkGenerator(HalfBakedLegacyLinkGenerator) { pluginManager = ref('pluginManager') } } - - grailsResourceProcessor(org.grails.plugin.resource.ResourceProcessor) { + + grailsResourceProcessor(ResourceProcessor) { grailsLinkGenerator = ref('grailsLinkGenerator') if (springConfig.containsBean('grailsResourceLocator')) { grailsResourceLocator = ref('grailsResourceLocator') } grailsApplication = ref('grailsApplication') } - + // Legacy service name springConfig.addAlias "resourceService", "grailsResourceProcessor" + + // register the backing Spring bean for each of the filters + for (f in findFilters(application)) { + "$f.name"(f.filterClass) { bean -> + bean.autowire = 'byName' + + f.params.each { k, v -> + delegate."$k" = v + } + } + } } - - def doWithWebDescriptor = { webXml -> - def adHocPatterns = getAdHocPatterns(application) - def declaredResFilter = [ - name:'DeclaredResourcesPluginFilter', - filterClass:"org.grails.plugin.resource.ProcessingFilter", - urlPatterns:["/${getUriPrefix(application)}/*"] - ] - def adHocFilter = [ - name:'AdHocResourcesPluginFilter', - filterClass:"org.grails.plugin.resource.ProcessingFilter", - params: [adhoc:true], - urlPatterns: adHocPatterns - ] + def doWithWebDescriptor = { webXml -> - def filtersToAdd = [declaredResFilter] - if (adHocPatterns) { - filtersToAdd << adHocFilter - } + def filtersToAdd = findFilters(application) - if ( Environment.current == Environment.DEVELOPMENT) { - filtersToAdd << [ - name:'ResourcesDevModeFilter', - filterClass:"org.grails.plugin.resource.DevModeSanityFilter", - urlPatterns:['/*'], - dispatchers:['REQUEST'] - ] - } - log.info("Adding servlet filters") def filters = webXml.filter[0] filters + { filtersToAdd.each { f -> - log.info "Adding filter: ${f.name} with class ${f.filterClass} and init-params: ${f.params}" + log.info "Adding filter: $f.name with class $f.filterClass.name and init-params: $f.params" 'filter' { 'filter-name'(f.name) - 'filter-class'(f.filterClass) - f.params?.each { k, v -> - 'init-param' { - 'param-name'(k) - 'param-value'(v.toString()) - } - } + 'filter-class'(DelegatingFilterProxy.name) } } } - def mappings = webXml.'filter-mapping'[0] + def mappings = webXml.'filter-mapping'[0] mappings + { filtersToAdd.each { f -> f.urlPatterns?.each { p -> - log.info "Adding url pattern ${p} for filter ${f.name}" + log.info "Adding url pattern $p for filter $f.name" 'filter-mapping' { 'filter-name'(f.name) 'url-pattern'(p) @@ -172,28 +161,16 @@ class ResourcesGrailsPlugin { applicationContext.grailsResourceProcessor.reloadAll() } - static PATH_MATCHER = new AntPathMatcher() - static RELOADABLE_RESOURCE_EXCLUDES = [ - '**/.svn/**/*.*', - '**/.git/**/*.*', - 'WEB-INF/**/*.*', - 'META-INF/**/*.*' - ] - boolean isResourceWeShouldProcess(File file) { // Make windows filenams safe for matching def fileName = file.canonicalPath.replaceAll('\\\\', '/').replaceAll('^.*?/web-app/', '') boolean shouldProcess = !(RELOADABLE_RESOURCE_EXCLUDES.any { PATH_MATCHER.match(it, fileName ) }) return shouldProcess } - - ScheduledThreadPoolExecutor delayedChangeThrottle = new ScheduledThreadPoolExecutor(1) - ScheduledFuture reloadTask - static final RELOAD_THROTTLE_DELAY = 500 - + void triggerReload(Closure reloader) { reloadTask?.cancel(false) - reloadTask = delayedChangeThrottle.schedule( { + reloadTask = delayedChangeThrottle.schedule( { try { reloader() } catch (Throwable t) { @@ -202,7 +179,7 @@ class ResourcesGrailsPlugin { } }, RELOAD_THROTTLE_DELAY, TimeUnit.MILLISECONDS) } - + def onChange = { event -> if (event.source instanceof FileSystemResource) { if (isResourceWeShouldProcess(event.source.file)) { @@ -224,25 +201,58 @@ class ResourcesGrailsPlugin { } } - protected handleChange(application, event, type, log) { - if (application.isArtefactOfType(type, event.source)) { - log.debug("reloading $event.source.name ($type)") - def oldClass = application.getArtefact(type, event.source.name) - application.addArtefact(type, event.source) - // Reload subclasses - if (oldClass) { - application.getArtefacts(type).each { - if (it.clazz != event.source && oldClass.clazz.isAssignableFrom(it.clazz)) { - def newClass = application.classLoader.reloadClass(it.clazz.name) - application.addArtefact(type, newClass) - } + protected boolean handleChange(application, event, type, log) { + if (!application.isArtefactOfType(type, event.source)) { + return false + } + + log.debug("reloading $event.source.name ($type)") + def oldClass = application.getArtefact(type, event.source.name) + application.addArtefact(type, event.source) + // Reload subclasses + if (oldClass) { + application.getArtefacts(type).each { + if (it.clazz != event.source && oldClass.clazz.isAssignableFrom(it.clazz)) { + def newClass = application.classLoader.reloadClass(it.clazz.name) + application.addArtefact(type, newClass) } } - - true - } else { - false } + + true + } + + private List findFilters(application) { + def adHocPatterns = getAdHocPatterns(application) + + def declaredResFilter = [ + name:'DeclaredResourcesPluginFilter', + filterClass: ProcessingFilter, + urlPatterns:["/${getUriPrefix(application)}/*"] + ] + + def filtersToAdd = [declaredResFilter] + + if (adHocPatterns) { + def adHocFilter = [ + name:'AdHocResourcesPluginFilter', + filterClass: ProcessingFilter, + params: [adhoc:true], + urlPatterns: adHocPatterns + ] + filtersToAdd << adHocFilter + } + + if (Environment.isDevelopmentMode()) { + filtersToAdd << [ + name:'ResourcesDevModeFilter', + filterClass: DevModeSanityFilter, + urlPatterns:['/*'], + dispatchers:['REQUEST'] + ] + } + + filtersToAdd } def onConfigChange = { event -> @@ -250,7 +260,7 @@ class ResourcesGrailsPlugin { } /** - * We have to soft load this class so this file can be compiled on it's own. + * We have to soft load this class so this file can be compiled on its own. */ static getResourceMapperArtefactHandler() { softLoadClass('org.grails.plugin.resources.artefacts.ResourceMapperArtefactHandler') diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 2d79a16..27a8102 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -1,40 +1,18 @@ -grails.project.class.dir = "target/classes" -grails.project.test.class.dir = "target/test-classes" -grails.project.test.reports.dir = "target/test-reports" -//grails.project.war.file = "target/${appName}-${appVersion}.war" +grails.project.work.dir = 'target' + grails.project.dependency.resolution = { - // inherit Grails' default dependencies - inherits( "global" ) { - // uncomment to disable ehcache - // excludes 'ehcache' - } - log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose' - repositories { - grailsCentral() - grailsRepo "http://grails.org/plugins" - // uncomment the below to enable remote dependency resolution - // from public Maven repositories - //mavenLocal() - //mavenCentral() - //mavenRepo "http://snapshots.repository.codehaus.org" - //mavenRepo "http://repository.codehaus.org" - //mavenRepo "http://download.java.net/maven/2/" - //mavenRepo "http://repository.jboss.com/maven2/" - } - dependencies { -// build 'org.codehaus.gpars:gpars:0.12' - } - plugins { - provided(":webxml:1.4.1") - build(":tomcat:$grailsVersion") { - export = false - } - build(":release:2.1.0") { - export = false - } - test(":spock:0.6"){ - export = false - } - } + inherits 'global' + log 'warn' + + repositories { + grailsCentral() + } + + plugins { + provided(":webxml:1.4.1") + test(":spock:0.6"){ + export = false + } + } } diff --git a/grails-app/conf/ResourcesBootStrap.groovy b/grails-app/conf/ResourcesBootStrap.groovy index e2c3dcf..393feba 100644 --- a/grails-app/conf/ResourcesBootStrap.groovy +++ b/grails-app/conf/ResourcesBootStrap.groovy @@ -1,7 +1,5 @@ import grails.util.Environment -import org.grails.plugin.resource.* - /** * Bootstraps the plugin by loading the app resources from config * @@ -9,17 +7,13 @@ import org.grails.plugin.resource.* * @author Luke Daley (ld@ldaley.com) */ class ResourcesBootStrap { - + def grailsResourceProcessor - + def init = { servletContext -> /*grailsResourceProcessor.reload()*/ if (Environment.current == Environment.DEVELOPMENT) { grailsResourceProcessor.dumpResources() } } - - def destroy = { - - } -} \ No newline at end of file +} diff --git a/grails-app/resourceMappers/org/grails/plugin/resource/BaseUrlResourceMapper.groovy b/grails-app/resourceMappers/org/grails/plugin/resource/BaseUrlResourceMapper.groovy index af97f85..9ee0c3a 100644 --- a/grails-app/resourceMappers/org/grails/plugin/resource/BaseUrlResourceMapper.groovy +++ b/grails-app/resourceMappers/org/grails/plugin/resource/BaseUrlResourceMapper.groovy @@ -3,49 +3,51 @@ package org.grails.plugin.resource import org.grails.plugin.resource.mapper.MapperPhase /** - * Mapper that applies an optional base url to resources, e.g. for putting content out to + * Mapper that applies an optional base url to resources, e.g. for putting content out to * one or more pull CDNs * @author Tomas Lin * @since 1.2 */ class BaseUrlResourceMapper { - static priority = 0 + static int priority = 0 - static phase = MapperPhase.ABSOLUTISATION + static MapperPhase phase = MapperPhase.ABSOLUTISATION def map(resource, config) { - if (config.enabled) { - def url + if (!config.enabled) { + return + } - if (isResourceBundle(resource)) { - verifySameBaseUrlForAllModulesInBundle(resource, config) - } + String url - String moduleName = getModuleName(resource) - if (moduleName && config.modules[moduleName]) { - url = config.modules[moduleName] - } + if (isResourceBundle(resource)) { + verifySameBaseUrlForAllModulesInBundle(resource, config) + } - if (!url) { - url = config.default - } + String moduleName = getModuleName(resource) + if (moduleName && config.modules[moduleName]) { + url = config.modules[moduleName] + } + + if (!url) { + url = config.default + } - if (url) { - if (url.endsWith('/')) { - url = url[0..-2] - } - resource.linkOverride = url + resource.linkUrl + if (url) { + if (url.endsWith('/')) { + url = url[0..-2] } + resource.linkOverride = url + resource.linkUrl } } void verifySameBaseUrlForAllModulesInBundle(AggregatedResourceMeta bundle, Map config) { def moduleNames = bundle.resources.collect this.&getModuleName def baseUrlsForBundleModules = moduleNames.collectEntries { [it, config.modules[it] ?: config.default] } - def uniqueUrls = baseUrlsForBundleModules.values().unique(false) + List uniqueUrls = baseUrlsForBundleModules.values().unique(false) if (uniqueUrls.size() != 1) { - def bundleName = bundle.resources.first().bundle + String bundleName = bundle.resources.first().bundle throw new IllegalArgumentException("All modules bundled together must have the same baseUrl override. " + "Offending bundle is [$bundleName] and baseUrl overrides for its' modules are: $baseUrlsForBundleModules") } @@ -61,4 +63,4 @@ class BaseUrlResourceMapper { private boolean isResourceBundle(resource) { resource instanceof AggregatedResourceMeta && resource.resources } -} \ No newline at end of file +} diff --git a/grails-app/resourceMappers/org/grails/plugin/resource/BundleResourceMapper.groovy b/grails-app/resourceMappers/org/grails/plugin/resource/BundleResourceMapper.groovy index 6f9fd36..1ee21dc 100644 --- a/grails-app/resourceMappers/org/grails/plugin/resource/BundleResourceMapper.groovy +++ b/grails-app/resourceMappers/org/grails/plugin/resource/BundleResourceMapper.groovy @@ -6,25 +6,25 @@ import org.grails.plugin.resource.mapper.MapperPhase * This mapper creates synthetic AggregatedResourceMeta instances for any bundle * names found in the resource declarations, and gathers up info about those resources * so that when the bundle itself is requested, the aggregated file is created and returned. - * + * * This sets any ResourceMeta to which this mapper applies, to be "delegating" to the new aggregated resource * so when those resources are rendered/requested, the bundle URI is written out. * * @author Marc Palmer (marc@grailsrocks.com) */ class BundleResourceMapper { - + def phase = MapperPhase.AGGREGATION - + def grailsResourceProcessor - + static MIMETYPE_TO_RESOURCE_META_CLASS = [ 'text/css': CSSBundleResourceMeta, 'text/javascript': JavaScriptBundleResourceMeta, 'application/javascript': JavaScriptBundleResourceMeta, 'application/x-javascript': JavaScriptBundleResourceMeta ] - + /** * Find resources that belong in bundles, and create the bundles, and make the resource delegate to the bundle. * Creates a new aggregated resource for the bundle and shoves all the resourceMetas into it. @@ -37,7 +37,7 @@ class BundleResourceMapper { if (bundleId) { def resType = MIMETYPE_TO_RESOURCE_META_CLASS[resource.contentType] if (!resType) { - log.warn "Cannot create a bundle from resource [${resource.sourceUrl}], "+ + log.warn "Cannot create a bundle from resource [${resource.sourceUrl}], " + "the content type [${resource.contentType}] is not supported. Set the resource to exclude bundle mapper." return } diff --git a/grails-app/resourceMappers/org/grails/plugin/resource/CSSPreprocessorResourceMapper.groovy b/grails-app/resourceMappers/org/grails/plugin/resource/CSSPreprocessorResourceMapper.groovy index 41d1d0a..2bc9faf 100644 --- a/grails-app/resourceMappers/org/grails/plugin/resource/CSSPreprocessorResourceMapper.groovy +++ b/grails-app/resourceMappers/org/grails/plugin/resource/CSSPreprocessorResourceMapper.groovy @@ -5,7 +5,7 @@ import org.grails.plugin.resource.mapper.MapperPhase /** * This mapper is the first phase of CSS rewriting. * - * It will find any relative URIs in the CSS and convert them to a "resource:" + * It will find any relative URIs in the CSS and convert them to a "resource:" * so that later after mappers have been applied, the URIs can be fixed up and restored to URIs relative to the * new CSS output file's location. For example a bundle or "hashandcache" mapper may move the CSS file to a completely * different place, thus breaking all the relative links to images. @@ -22,7 +22,7 @@ class CSSPreprocessorResourceMapper { static defaultIncludes = ['**/*.css'] def grailsResourceProcessor - + /** * Find all url() and fix up the url if it is not absolute * NOTE: This needs to run after any plugins that move resources around, but before any that obliterate @@ -31,24 +31,24 @@ class CSSPreprocessorResourceMapper { def map(resource, config) { if (resource instanceof AggregatedResourceMeta) { if (log.debugEnabled) { - log.debug "CSS Preprocessor skipping ${resource} because it is aggregated (already processed each file in it)" + log.debug "CSS Preprocessor skipping $resource because it is aggregated (already processed each file in it)" } return null } - + def processor = new CSSLinkProcessor() - + if (log.debugEnabled) { log.debug "CSS Preprocessor munging ${resource}" } processor.process(resource, grailsResourceProcessor) { prefix, originalUrl, suffix -> - + if (log.debugEnabled) { log.debug "CSS Preprocessor munging url $originalUrl" } - - // We don't do absolutes or full URLs - perhaps we should do "/" at some point? If app + + // We don't do absolutes or full URLs - perhaps we should do "/" at some point? If app // is mapped to root context then some people might do this but its lame // Also skip already-processed resources (i.e. bundled CSS) if (!URLUtils.isRelativeURL(originalUrl)) { @@ -81,11 +81,10 @@ class CSSPreprocessorResourceMapper { log.debug "Calculated absoluted URI of CSS resource [$originalUrl] as [$uri]" } return "${prefix}${uri}${suffix}" - } else { - return "${prefix}${originalUrl}${suffix}" } + return "${prefix}${originalUrl}${suffix}" } return null } -} \ No newline at end of file +} diff --git a/grails-app/resourceMappers/org/grails/plugin/resource/CSSRewriterResourceMapper.groovy b/grails-app/resourceMappers/org/grails/plugin/resource/CSSRewriterResourceMapper.groovy index 13deaff..0684668 100644 --- a/grails-app/resourceMappers/org/grails/plugin/resource/CSSRewriterResourceMapper.groovy +++ b/grails-app/resourceMappers/org/grails/plugin/resource/CSSRewriterResourceMapper.groovy @@ -16,13 +16,13 @@ import org.grails.plugin.resource.mapper.MapperPhase class CSSRewriterResourceMapper { // def priority = 1000 - + def phase = MapperPhase.LINKREALISATION - + def grailsResourceProcessor - + static defaultIncludes = ['**/*.css'] - + /** * Find all url() and fix up the url if it is not absolute * NOTE: This needs to run after any plugins that move resources around, but before any that obliterate @@ -34,10 +34,10 @@ class CSSRewriterResourceMapper { def processor = new CSSLinkProcessor() processor.process(resource, grailsResourceProcessor) { prefix, originalUrl, suffix -> - + if (originalUrl.startsWith('resource:')) { def uri = originalUrl - 'resource:' - + if (log.debugEnabled) { log.debug "Calculated URI of CSS resource [$originalUrl] as [$uri]" } @@ -53,14 +53,12 @@ class CSSRewriterResourceMapper { if (log.debugEnabled) { log.debug "Calculating URL of ${linkedToResource?.dump()} relative to ${resource.dump()}" } - def fixedUrl = linkedToResource.relativeToWithQueryParams(resource) def replacement = "${prefix}${fixedUrl}${suffix}" if (log.debugEnabled) { log.debug "Rewriting CSS URL '${originalUrl}' to '$replacement'" } - return replacement } else { log.warn "Cannot resolve CSS resource, leaving link as is: ${originalUrl}" @@ -69,4 +67,4 @@ class CSSRewriterResourceMapper { return "${prefix}${originalUrl}${suffix}" } } -} \ No newline at end of file +} diff --git a/grails-app/resourceMappers/org/grails/plugin/resource/test/TestResourceMapper.groovy b/grails-app/resourceMappers/org/grails/plugin/resource/test/TestResourceMapper.groovy index 2087a6f..1129922 100644 --- a/grails-app/resourceMappers/org/grails/plugin/resource/test/TestResourceMapper.groovy +++ b/grails-app/resourceMappers/org/grails/plugin/resource/test/TestResourceMapper.groovy @@ -1,16 +1,16 @@ package org.grails.plugin.resource.test +import org.grails.plugin.resource.ResourceMeta import org.grails.plugin.resource.mapper.MapperPhase class TestResourceMapper { def phase = MapperPhase.MUTATION - def map(resource, config) { + def map(ResourceMeta resource, config) { def file = new File(resource.processedFile.parentFile, "_${resource.processedFile.name}") assert resource.processedFile.renameTo(file) resource.processedFile = file resource.updateActualUrlFromProcessedFile() } - -} \ No newline at end of file +} diff --git a/grails-app/taglib/org/grails/plugin/resource/ResourceTagLib.groovy b/grails-app/taglib/org/grails/plugin/resource/ResourceTagLib.groovy index ae83ec9..3f91968 100644 --- a/grails-app/taglib/org/grails/plugin/resource/ResourceTagLib.groovy +++ b/grails-app/taglib/org/grails/plugin/resource/ResourceTagLib.groovy @@ -1,14 +1,10 @@ package org.grails.plugin.resource -import grails.util.Environment import grails.util.GrailsUtil -import org.codehaus.groovy.grails.commons.ConfigurationHolder import org.apache.commons.io.FilenameUtils -import org.grails.plugin.resource.util.HalfBakedLegacyLinkGenerator import org.codehaus.groovy.grails.web.taglib.exceptions.GrailsTagException - /** * This taglib handles creation of all the links to resources, including the smart de-duping of them. * @@ -18,60 +14,60 @@ import org.codehaus.groovy.grails.web.taglib.exceptions.GrailsTagException * @author Luke Daley (ld@ldaley.com) */ class ResourceTagLib { - + static namespace = "r" - - static REQ_ATTR_PREFIX_PAGE_FRAGMENTS = 'resources.plugin.page.fragments' - static REQ_ATTR_PREFIX_AUTO_DISPOSITION = 'resources.plugin.auto.disposition' - - static stashWriters = [ - 'script': { out, stash -> - out << "" + static final String REQ_ATTR_PREFIX_PAGE_FRAGMENTS = 'resources.plugin.page.fragments' + static final String REQ_ATTR_PREFIX_AUTO_DISPOSITION = 'resources.plugin.auto.disposition' + private static final List fragmentTypes = ['script', 'style'] + + static stashWriters = [ + script: { out, stash -> + writeStash '', out, stash }, - 'style': { out, stash -> - out << "" + style: { out, stash -> + writeStash '', out, stash } ] - + + protected static void writeStash(String start, String end, out, stash) { + out << start + for (s in stash) { + out << s + } + out << end + } + static writeAttrs( attrs, output) { // Output any remaining user-specified attributes attrs.each { k, v -> if (v != null) { - output << k - output << '="' - output << v.encodeAsHTML() - output << '" ' - } + output << k + output << '="' + output << v.encodeAsHTML() + output << '" ' + } } } - // Closures to write links of different types + // Closures to write links of different types static LINK_WRITERS = [ js: { url, constants, attrs -> def o = new StringBuilder() - o << "' - return o + return o }, - + link: { url, constants, attrs -> def o = new StringBuilder() - o << " @@ -334,15 +330,15 @@ class ResourceTagLib { declareModuleRequiredByPage(name, mandatory) } } - - private stashPageFragment(String type, String disposition, def fragment) { + + private stashPageFragment(String type, String disposition, fragment) { if (log.debugEnabled) { - log.debug "stashing request script for disposition [${disposition}]: ${ fragment}" + log.debug "stashing request script for disposition [$disposition]: $fragment" } needsResourceLayout() - - def trkName = ResourceTagLib.makePageFragmentKey(type, disposition) + + def trkName = makePageFragmentKey(type, disposition) grailsResourceProcessor.addDispositionToRequest(request, disposition, '-page-fragments-') def trk = request[trkName] @@ -352,28 +348,27 @@ class ResourceTagLib { } trk << fragment } - + private static String makePageFragmentKey(String type, String disposition) { "${REQ_ATTR_PREFIX_PAGE_FRAGMENTS}:${type}:${disposition}" } - + private consumePageFragments(String type, String disposition) { - def fraggles = request[ResourceTagLib.makePageFragmentKey(type, disposition)] ?: Collections.EMPTY_LIST - return fraggles + request[makePageFragmentKey(type, disposition)] ?: Collections.EMPTY_LIST } - + private static String makeAutoDispositionKey( String disposition) { "${REQ_ATTR_PREFIX_AUTO_DISPOSITION}:${disposition}" } private isAutoLayoutDone(disposition) { - request[ResourceTagLib.makeAutoDispositionKey(disposition)] + request[makeAutoDispositionKey(disposition)] } - + private autoLayoutDone(disposition) { - request[ResourceTagLib.makeAutoDispositionKey(disposition)] = true + request[makeAutoDispositionKey(disposition)] = true } - + /** * Render the resources. First invocation renders head JS and CSS, second renders deferred JS only, and any more spews. */ @@ -398,17 +393,18 @@ class ResourceTagLib { } return } - } else if (!remainingDispositions.contains(dispositionToRender)) { + } + else if (!remainingDispositions.contains(dispositionToRender)) { if (log.warnEnabled) { log.warn "A request was made to render resources for disposition [${dispositionToRender}] but there are no resources scheduled for that disposition, or it has already been rendered" } return } - + if (log.debugEnabled) { log.debug "Rendering resources for disposition [${dispositionToRender}]" } - + def trk = request.resourceModuleTracker if (log.debugEnabled) { log.debug "Rendering resources, modules in tracker: ${trk}" @@ -417,32 +413,28 @@ class ResourceTagLib { if (log.debugEnabled) { log.debug "Rendering resources, modules needed: ${modulesNeeded}" } - def modulesInOrder = grailsResourceProcessor.getModulesInDependencyOrder(modulesNeeded) if (log.debugEnabled) { log.debug "Rendering non-deferred resources, modules: ${modulesInOrder}..." } - for (module in modulesInOrder) { // @todo where a module resource is bundled, need to satisfy deps of all resources in the bundle first! out << r.renderModule(name:module, disposition:dispositionToRender) } - + if (log.debugEnabled) { log.debug "Rendering page fragments for disposition: ${dispositionToRender}" } - layoutPageStash(dispositionToRender) if (log.debugEnabled) { log.debug "Removing outstanding request disposition: ${dispositionToRender}" } - + grailsResourceProcessor.doneDispositionResources(request, dispositionToRender) } private layoutPageStash(String disposition) { - def fragmentTypes = ['script', 'style'] for (t in fragmentTypes) { def stash = consumePageFragments(t, disposition) if (stash) { @@ -463,7 +455,7 @@ class ResourceTagLib { def stash = { attrs, body -> stashPageFragment(attrs.type, attrs.disposition, body()) } - + protected getModuleByName(name) { def module = grailsResourceProcessor.getModule(name) if (!module) { @@ -491,23 +483,23 @@ class ResourceTagLib { if (!module) { return } - + def s = new StringBuilder() - + def renderingDisposition = attrs.remove('disposition') if (log.debugEnabled) { log.debug "Rendering the resources of module [${name}]: ${module.dump()}" } - + def debugMode = grailsResourceProcessor.isDebugMode(request) - - for (r in module.resources) { + + for (r in module.resources) { if (!r.exists() && !r.actualUrl?.contains('://')) { - throw new IllegalArgumentException("Module [$name] depends on resource [${r.sourceUrl}] but the file cannot be found") + throw new IllegalArgumentException("Module [$name] depends on resource [$r.sourceUrl] but the file cannot be found") } if (log.debugEnabled) { - log.debug "Resource: ${r.sourceUrl} - disposition ${r.disposition} - rendering disposition ${renderingDisposition}" + log.debug "Resource: $r.sourceUrl - disposition $r.disposition - rendering disposition ${renderingDisposition}" } if (r.disposition == renderingDisposition) { def args = [:] @@ -515,11 +507,11 @@ class ResourceTagLib { args.uri = debugMode ? r.originalUrl : "${r.actualUrl}" args.wrapper = r.prePostWrapper args.disposition = r.disposition - + if (r.tagAttributes) { args.putAll(r.tagAttributes) // Copy the attrs originally provided e.g. type override } - + if (log.debugEnabled) { log.debug "Rendering one of the module's resource links: ${args}" } @@ -532,14 +524,14 @@ class ResourceTagLib { /** * Get the uri to use for linking, and - if relevant - the resource instance. - * - * NOTE: The URI handling mechanics in here are pretty evil and nuanced (i.e. + * + * NOTE: The URI handling mechanics in here are pretty evil and nuanced (i.e. * ad-hoc vs declared, ad-hoc and not found, ad-hoc and excluded etc). * There is reasonable test coverage, but only fools rush in. - * + * * @attr uri - to be resolved, i.e. id of the ResourceMeta * @attr disposition - of the resource - * + * * @return Map with uri/url property and *maybe* a resource property */ def resolveLinkUriToUriAndResource(attrs) { @@ -556,30 +548,30 @@ class ResourceTagLib { // via g.resource attrs.contextPath = ctxPath uri = grailsLinkGenerator.resource(attrs) - abs = uri.contains('://') + abs = uri.contains('://') } else { if (!abs) { uri = ctxPath + uri } } - + def debugMode = grailsResourceProcessor.isDebugMode(request) // Get out quick and add param to tell filter we don't want any fancy stuff if (debugMode) { - + // Some JS libraries can't handle different query params being sent to other dependencies // so we reuse the same timestamp for the lifecycle of the request - - // Here we allow a refresh arg that will generate a new timestamp, normally we used the last we - // generated. Otherwise, you can't debug anything in a JS debugger as the URI of the JS + + // Here we allow a refresh arg that will generate a new timestamp, normally we used the last we + // generated. Otherwise, you can't debug anything in a JS debugger as the URI of the JS // is different every time. if (params._refreshResources && !request.'grails-resources.debug-timestamp-refreshed') { // Force re-generation of a new timestamp in debug mode session.removeAttribute('grails-resources.debug-timestamp') request.'grails-resources.debug-timestamp-refreshed' = true } - + def timestamp = session['grails-resources.debug-timestamp'] if (!timestamp) { timestamp = System.currentTimeMillis() @@ -588,14 +580,14 @@ class ResourceTagLib { uri += (uri.indexOf('?') >= 0) ? "&_debugResources=y&n=$timestamp" : "?_debugResources=y&n=$timestamp" return [uri:uri, debug:true] - } - + } + def disposition = attrs.remove('disposition') if (!abs) { uri = forcePrefixedWithSlash(uri) } - + // If its a bad or empty URI, get out of here. It must at least contain the context path if it is relative if (!abs && (uri.size() <= ctxPath.size())) { return [uri:uri] @@ -603,7 +595,7 @@ class ResourceTagLib { def contextRelUri = abs ? uri : uri[ctxPath.size()..-1] def reluri = ResourceProcessor.removeQueryParams(contextRelUri) - + // Get ResourceMeta or create one if uri is not absolute def res = grailsResourceProcessor.getExistingResourceMeta(reluri) if (!res && !abs) { @@ -616,19 +608,19 @@ class ResourceTagLib { } // If the link has to support linkUrl for override, or fall back to the full requested url - // we resolve without query params, but must keep them for linking + // we resolve without query params, but must keep them for linking def linkUrl = res ? res.linkUrl : contextRelUri if (linkUrl.contains('://')) { // @todo do we need to toggle http/https here based on current request protocol? return [uri:linkUrl, resource:res] - } else { - // Only apply static prefix if the resource actually has ResourceMeta created for it - uri = res ? ctxPath+grailsResourceProcessor.staticUrlPrefix+linkUrl : ctxPath+linkUrl - return [uri:uri, resource:res] } + + // Only apply static prefix if the resource actually has ResourceMeta created for it + uri = res ? ctxPath+grailsResourceProcessor.staticUrlPrefix+linkUrl : ctxPath+linkUrl + [uri:uri, resource:res] } - + /** * Get the URL for a resource * @todo this currently won't work for absolute="true" invocations, it should just passthrough these @@ -646,7 +638,7 @@ class ResourceTagLib { out << info.uri } } - + /** * Write out an HTML tag using resource processing for the image */ @@ -656,15 +648,15 @@ class ResourceTagLib { } def args = attrs.clone() args.disposition = "image" - + def info = resolveLinkUriToUriAndResource(args) def res = info.resource attrs.remove('uri') def o = new StringBuilder() - o << "" out << o } - + protected forcePrefixedWithSlash(uri) { if (uri) { if (uri[0] != '/') { diff --git a/grails-app/views/index.gsp b/grails-app/views/index.gsp index 50fbb66..cf411c7 100644 --- a/grails-app/views/index.gsp +++ b/grails-app/views/index.gsp @@ -18,7 +18,7 @@ - + ${output} @@ -31,7 +31,7 @@ ${output.encodeAsHTML()}

Grails logo using processing:

- + document.write("

This is going to come out in the footer

"); diff --git a/src/groovy/org/grails/plugin/resource/AggregatedResourceMeta.groovy b/src/groovy/org/grails/plugin/resource/AggregatedResourceMeta.groovy index 205c750..1f64176 100644 --- a/src/groovy/org/grails/plugin/resource/AggregatedResourceMeta.groovy +++ b/src/groovy/org/grails/plugin/resource/AggregatedResourceMeta.groovy @@ -1,9 +1,5 @@ package org.grails.plugin.resource -import org.apache.commons.logging.LogFactory - -import org.apache.commons.io.FilenameUtils - /** * Holder for info about a resource that is made up of other resources * @@ -12,19 +8,13 @@ import org.apache.commons.io.FilenameUtils */ class AggregatedResourceMeta extends ResourceMeta { - def log = LogFactory.getLog(this.class) - - def resources = [] - def inheritedModuleDependencies = new HashSet() - - void reset() { - super.reset() - } + List resources = [] + Set inheritedModuleDependencies = [] boolean containsResource(ResourceMeta r) { resources.find { r.sourceUrl == it.sourceUrl } } - + @Override boolean isDirty() { resources.any { it.dirty } @@ -36,7 +26,7 @@ class AggregatedResourceMeta extends ResourceMeta { if (!containsResource(r)) { resources << r inheritedModuleDependencies << r.module - + // Update our aggregated sourceUrl sourceUrl = "${sourceUrl}, ${r.sourceUrl}" } @@ -50,8 +40,8 @@ class AggregatedResourceMeta extends ResourceMeta { processedFile.newWriter('UTF-8', true) } - protected initFile(grailsResourceProcessor) { - def commaPos = sourceUrl.indexOf(',') + protected void initFile(grailsResourceProcessor) { + int commaPos = sourceUrl.indexOf(',') if (commaPos == -1) { commaPos = sourceUrl.size() } @@ -60,48 +50,48 @@ class AggregatedResourceMeta extends ResourceMeta { processedFile = grailsResourceProcessor.makeFileForURI(actualUrl) processedFile.createNewFile() - this.contentType = grailsResourceProcessor.getMimeType(actualUrl) + contentType = grailsResourceProcessor.getMimeType(actualUrl) } @Override void beginPrepare(grailsResourceProcessor) { initFile(grailsResourceProcessor) - this.originalSize = resources.originalSize.sum() - + originalSize = resources.originalSize.sum() + buildAggregateResource(grailsResourceProcessor) } void buildAggregateResource(grailsResourceProcessor) { - def moduleOrder = grailsResourceProcessor.modulesInDependencyOrder + List moduleOrder = grailsResourceProcessor.modulesInDependencyOrder + + long newestLastMod = 0 - def newestLastMod = 0 - def bundledContent = new StringBuilder() - + // Add the resources to the file in the order determined by module dependencies! - moduleOrder.each { m -> - resources.each { r -> + for (String m in moduleOrder) { + for (ResourceMeta r in resources) { if (r.module.name == m) { // Append to the existing file if (log.debugEnabled) { - log.debug "Appending contents of ${r.processedFile} to ${processedFile}" + log.debug "Appending contents of $r.processedFile to $processedFile" } bundledContent << r.processedFile.getText("UTF-8") bundledContent << "\r\n" - + if (r.originalLastMod > newestLastMod) { newestLastMod = r.originalLastMod } } } } - + def out = getWriter() out << bundledContent out << "\r\n" out.close() - - this.originalLastMod = newestLastMod + + originalLastMod = newestLastMod } -} \ No newline at end of file +} diff --git a/src/groovy/org/grails/plugin/resource/CSSBundleResourceMeta.groovy b/src/groovy/org/grails/plugin/resource/CSSBundleResourceMeta.groovy index 9eb510c..3bc2b72 100644 --- a/src/groovy/org/grails/plugin/resource/CSSBundleResourceMeta.groovy +++ b/src/groovy/org/grails/plugin/resource/CSSBundleResourceMeta.groovy @@ -1,9 +1,5 @@ package org.grails.plugin.resource -import org.apache.commons.logging.LogFactory - -import org.apache.commons.io.FilenameUtils - /** * Holder for info about a resource that is made up of other resources * @@ -12,16 +8,14 @@ import org.apache.commons.io.FilenameUtils */ class CSSBundleResourceMeta extends AggregatedResourceMeta { - def log = LogFactory.getLog(this.class) - @Override - void beginPrepare(grailsResourceProcessor) { + void beginPrepare(ResourceProcessor grailsResourceProcessor) { initFile(grailsResourceProcessor) - + def out = getWriter() out << '@charset "UTF-8";\n' out.close() buildAggregateResource(grailsResourceProcessor) } -} \ No newline at end of file +} diff --git a/src/groovy/org/grails/plugin/resource/CSSLinkProcessor.groovy b/src/groovy/org/grails/plugin/resource/CSSLinkProcessor.groovy index c9e1b2f..d22025c 100644 --- a/src/groovy/org/grails/plugin/resource/CSSLinkProcessor.groovy +++ b/src/groovy/org/grails/plugin/resource/CSSLinkProcessor.groovy @@ -1,68 +1,69 @@ package org.grails.plugin.resource -import org.apache.commons.logging.LogFactory +import org.slf4j.Logger +import org.slf4j.LoggerFactory /** * This class is used to parse out and replace CSS links - * + * * @author Marc Palmer (marc@grailsrocks.com) * @author Luke Daley (ld@ldaley.com) */ class CSSLinkProcessor { - - def log = LogFactory.getLog(CSSLinkProcessor) - + + private Logger log = LoggerFactory.getLogger(getClass()) + // We need to successfully match any kind of @import and url(), mappers are responsible for checking type - static CSS_URL_PATTERN = ~/(?:(@import\s*['"])(.+?)(['"]))|(url\(\s*['"]?)(.+?)(['"]?\s*\))/ - - boolean isCSSRewriteCandidate(resource, grailsResourceProcessor) { - def enabled = grailsResourceProcessor.config.rewrite.css instanceof Boolean ? grailsResourceProcessor.config.rewrite.css : true - def yes = enabled && (resource.contentType == "text/css" || resource.tagAttributes?.type == "css") + static final CSS_URL_PATTERN = ~/(?:(@import\s*['"])(.+?)(['"]))|(url\(\s*['"]?)(.+?)(['"]?\s*\))/ + + boolean isCSSRewriteCandidate(ResourceMeta resource, ResourceProcessor grailsResourceProcessor) { + boolean enabled = grailsResourceProcessor.config.rewrite.css instanceof Boolean ? grailsResourceProcessor.config.rewrite.css : true + boolean yes = enabled && (resource.contentType == "text/css" || resource.tagAttributes?.type == "css") if (log.debugEnabled) { - log.debug "Resource ${resource.actualUrl} being CSS rewritten? $yes" + log.debug "Resource $resource.actualUrl being CSS rewritten? $yes" } return yes } - + /** * Find all url() and fix up the url if it is not absolute * NOTE: This needs to run after any plugins that move resources around, but before any that obliterate * the content i.e. before minify or gzip */ - void process(ResourceMeta resource, grailsResourceProcessor, Closure urlMapper) { - + void process(ResourceMeta resource, ResourceProcessor grailsResourceProcessor, Closure urlMapper) { + if (!isCSSRewriteCandidate(resource, grailsResourceProcessor)) { if (log.debugEnabled) { - log.debug "CSS link processor skipping ${resource} because its not a CSS rewrite candidate" + log.debug "CSS link processor skipping $resource because its not a CSS rewrite candidate" } return } - + // Move existing to tmp file, then write to the correct file - def origFileTempCopy = new File(resource.processedFile.toString()+'.tmp') - + File origFileTempCopy = new File(resource.processedFile.toString()+'.tmp') + // Make sure temp file doesn't exist already new File(origFileTempCopy.toString()).delete() // On MS Windows if we don't do this origFileTempCopy gets corrupt after delete - + // Move the existing file to temp resource.processedFile.renameTo(origFileTempCopy) if (log.debugEnabled) { - log.debug "Pre-processing CSS resource ${resource.sourceUrl} to rewrite links" + log.debug "Pre-processing CSS resource $resource.sourceUrl to rewrite links" } - def inputCss = origFileTempCopy.getText('UTF-8') + String inputCss = origFileTempCopy.getText('UTF-8') def processedCss = inputCss.replaceAll(CSS_URL_PATTERN) { Object[] args -> - int modifier = args[1] ? 0 : 3 // determine: @import or url() match - def prefix = args[1 + modifier] - def originalUrl = args[2 + modifier].trim() - def suffix = args[3 + modifier] + int modifier = args[1] ? 0 : 3 // determine: @import or url() match + def prefix = args[1 + modifier] + String originalUrl = args[2 + modifier].trim() + def suffix = args[3 + modifier] - return urlMapper(prefix, originalUrl, suffix) + return urlMapper(prefix, originalUrl, suffix) } resource.processedFile.setText(processedCss, 'UTF-8') - + // Delete the temp file - origFileTempCopy.delete() + origFileTempCopy.delete() } -} \ No newline at end of file +} diff --git a/src/groovy/org/grails/plugin/resource/DevModeSanityFilter.groovy b/src/groovy/org/grails/plugin/resource/DevModeSanityFilter.groovy index 933be85..df79c11 100644 --- a/src/groovy/org/grails/plugin/resource/DevModeSanityFilter.groovy +++ b/src/groovy/org/grails/plugin/resource/DevModeSanityFilter.groovy @@ -1,19 +1,24 @@ package org.grails.plugin.resource -import javax.servlet.* -import org.springframework.web.context.support.WebApplicationContextUtils -import grails.util.Environment +import javax.servlet.FilterChain +import javax.servlet.FilterConfig +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +import org.springframework.web.filter.OncePerRequestFilter /** * This just traps any obvious mistakes the user has made and warns them in dev mode - * + * * @author Marc Palmer (marc@grailsrocks.com) */ -class DevModeSanityFilter implements Filter { - static RELOADING_DOC = """ +class DevModeSanityFilter extends OncePerRequestFilter { + + static final String RELOADING_DOC = """ - +