-
Notifications
You must be signed in to change notification settings - Fork 51
/
ResourceMeta.groovy
484 lines (398 loc) · 14.1 KB
/
ResourceMeta.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
package org.grails.plugin.resource
import java.net.URL;
import org.slf4j.LoggerFactory
import org.springframework.core.io.Resource
import org.springframework.core.io.UrlResource
import org.apache.commons.io.FilenameUtils
import org.springframework.core.io.FileSystemResource
import org.grails.plugin.resource.mapper.ResourceMapper
/**
* Holder for info about a resource declaration at runtime
*
* This is actually non-trivial. A lot of data kept here. Be wary of what you think a "url" is.
* See the javadocs for each URL property.
*
* @author Marc Palmer (marc@grailsrocks.com)
* @author Luke Daley (ld@ldaley.com)
*/
class ResourceMeta {
static final PROCESSED_BY_PREFIX = 'processed.by.'
def log = LoggerFactory.getLogger(ResourceMeta)
/**
* The optional module-unique id
*/
String id
/**
* The owning module
*/
ResourceModule module
/**
* Set on instantiation to be the dir that content is served from
*
* @see ResourceProcessor#workDir
*/
File workDir
/**
* The original Url provided in the mapping declaration, verbatim
*/
String originalUrl
/**
* The app-relative url of the LOCAL source of this resource, minus query params
*/
String sourceUrl
/**
* The original file extension of the resource
*/
String sourceUrlExtension
/**
* The original sourceUrlParamsAndFragment of the resource, if any
*/
String sourceUrlParamsAndFragment
/**
* The url of the local resource, after processing. (no query params)
*/
String actualUrl
/**
* The url to use when rendering links - e.g. for absolute CDN overrides
*/
String linkOverride
String bundle
/**
* The original mime type
*/
String contentType
/**
* Where do you want this resource? "defer", "head" etc
*/
String disposition
Set excludedMappers
// For per-resource options like "nominify", 'nozip'
Map attributes = [:]
// For per-resource tag resource attributes like "media", 'width', 'height' etc
Map tagAttributes = [:]
Closure prePostWrapper
// ***** Below here is state we determine at runtime during processing *******
/**
* The delegate to actually use when linking, if any. Think bundling.
*/
private ResourceMeta delegate
Resource originalResource
Long originalSize
Long processedSize
File processedFile
long originalLastMod
// A list of Closures taking request & response. Delegates to resourceMeta
List requestProcessors = []
private String _linkUrl
private boolean processed
private Boolean _resourceExists
/**
* The URI of the resource that resulted in the processing of this resource, or null
* For resources ref'd in CSS or stuff loaded up by bundles for example
*/
String declaringResource
Integer contentLength
Integer originalContentLength = 0
void delegateTo(ResourceMeta target) {
delegate = target
// No more processing to be done on us
processed = true
}
boolean isOriginalAbsolute() {
sourceUrl.indexOf(':/') > 0
}
boolean isActualAbsolute() {
actualUrl.indexOf(':/') > 0
}
boolean isDirty() {
!originalResource ||
(originalResource.lastModified() != originalLastMod)
}
boolean needsProcessing() {
processed
}
void updateContentLength() {
if (processedFile) {
this.@contentLength = processedFile.size().toInteger()
} else if (originalResource && originalResource.URL.protocol in ['jndi', 'file']) {
this.@contentLength = getOriginalResourceLength()
} else {
this.@contentLength = 0
}
}
long getOriginalResourceLength() {
if(originalResource) {
if (originalResource instanceof FileSystemResource) {
return originalResource.file.size()
} else {
// This may not close the connection in a timely manner if non-HTTP URL
return originalResource.URL.openConnection().contentLength
}
} else {
return 0
}
}
void setOriginalResource(Resource res) {
this.originalResource = res
updateExists()
this.originalContentLength = getOriginalResourceLength()
updateContentLength()
}
void setProcessedFile(File f) {
this.processedFile = f
updateExists()
updateContentLength()
}
void updateExists() {
if (processedFile) {
_resourceExists = processedFile.exists()
if (!this.originalLastMod && _resourceExists) {
this.originalLastMod = processedFile.lastModified()
}
if (this.originalSize == null) {
this.originalSize = _resourceExists ? processedFile.length() : 0
}
} else if (originalResource) {
_resourceExists = originalResource.exists()
if (!this.originalLastMod && _resourceExists) {
this.originalLastMod = originalResource.lastModified()
}
}
}
private void copyOriginalResourceToWorkArea() {
def inputStream = this.originalResource.inputStream
try {
// Now copy in the resource from this app deployment into the cache, ready for mutation
this.processedFile << inputStream
_resourceExists = this.processedFile.exists()
} finally {
inputStream?.close()
}
}
/**
* Return a new input stream for serving the resource - if processing is disabled
* the processedFile will be null and the original resource is used
*/
InputStream newInputStream() {
return processedFile ? processedFile.newInputStream() : originalResource.inputStream
}
// Hook for when preparation is starting
void beginPrepare(grailsResourceProcessor) {
def uri = this.sourceUrl
if (!URLUtils.isExternalURL(uri)) {
// Delete whatever file may already be there
processedFile?.delete()
def uriWithoutFragment = uri
if (uri.contains('#')) {
uriWithoutFragment = uri.substring(0, uri.indexOf('#'))
}
def origResourceURL = grailsResourceProcessor.getOriginalResourceURLForURI(uriWithoutFragment)
if (!origResourceURL) {
if (log.errorEnabled) {
if (this.declaringResource) {
log.error "While processing ${this.declaringResource}, a resource was required but not found: ${uriWithoutFragment}"
} else {
log.error "Resource not found: ${uriWithoutFragment}"
}
}
throw new FileNotFoundException("Cannot locate resource [$uri]")
}
this.contentType = grailsResourceProcessor.getMimeType(uriWithoutFragment)
if (log.debugEnabled) {
log.debug "Resource [$uriWithoutFragment] ($origResourceURL) has content type [${this.contentType}]"
}
setOriginalResource(new UrlResource(origResourceURL))
if (grailsResourceProcessor.processingEnabled) {
setActualUrl(uriWithoutFragment)
setProcessedFile(grailsResourceProcessor.makeFileForURI(uriWithoutFragment))
// copy the file ready for mutation
this.copyOriginalResourceToWorkArea()
} else {
setActualUrl(uriWithoutFragment)
}
} else {
def sourceUrlAsUrl = new URL(uri)
if(sourceUrlAsUrl.protocol in ['http', 'https']) {
def urlResource = new UrlResource(sourceUrlAsUrl)
setOriginalResource()
setActualUrl(uri)
log.warn "Skipping mappers for ${this.actualUrl} because its an absolute URL."
} else {
throw new FileNotFoundException("Cannot locate resource [$uri]")
}
}
}
// Hook for when preparation is done
void endPrepare(grailsResourceProcessor) {
if (!delegating) {
if (processedFile) {
processedFile.setLastModified(originalLastMod ?: System.currentTimeMillis() )
}
}
updateContentLength()
updateExists()
processed = true
}
boolean isDelegating() {
delegate != null
}
boolean exists() {
_resourceExists
}
String getLinkUrl() {
if (!delegate) {
return linkOverride ?: _linkUrl
} else {
return delegate.linkUrl
}
}
String getActualUrl() {
if (!delegate) {
return this.@actualUrl
} else {
return delegate.actualUrl
}
}
void setActualUrl(String url) {
this.@actualUrl = url
_linkUrl = sourceUrlParamsAndFragment ? actualUrl + sourceUrlParamsAndFragment : url
}
void setSourceUrl(String url) {
if (this.@originalUrl == null) {
this.@originalUrl = url // the full monty
}
def qidx = url.indexOf('?')
def hidx = url.indexOf('#')
def chopIdx = -1
// if there's hash we chop there, it comes before query
if (hidx >= 0 && qidx < 0) {
chopIdx = hidx
}
// if query params, we chop there even if have hash. Hash is after query params
if (qidx >= 0) {
chopIdx = qidx
}
sourceUrl = chopIdx >= 0 ? url[0..chopIdx-1] : url
// Strictly speaking this is query params plus fragment ...
sourceUrlParamsAndFragment = chopIdx >= 0 ? url[chopIdx..-1] : null
sourceUrlExtension = FilenameUtils.getExtension(sourceUrl) ?: null
}
/**
* The file extension of the processedFile, or null if it has no extension.
*/
String getProcessedFileExtension() {
if (processedFile) {
FilenameUtils.getExtension(processedFile.name) ?: null
}
}
String getWorkDirRelativeParentPath() {
workDirRelativePath - "$processedFile.name"
}
String getWorkDirRelativePath() {
if (processedFile) {
return processedFile.path - workDir.path
} else {
return null
}
}
String getActualUrlParent() {
def lastSlash = actualUrl.lastIndexOf('/')
if (lastSlash >= 0) {
return actualUrl[0..lastSlash-1]
} else {
return ''
}
}
String relativeToWithQueryParams(ResourceMeta base) {
def url = relativeTo(base)
return sourceUrlParamsAndFragment ? url + sourceUrlParamsAndFragment : url
}
ResourceMeta getDelegate() {
delegate
}
/**
* Reset the resource state to how it was after loading from the module definition
* i.e. keep only declared info, nothing generated later during processing
* // @todo should we delete the file in here?
*/
void reset() {
this.@contentType = null
this.@actualUrl = null
this.@processedFile = null
this.@originalResource = null
this.@_resourceExists = false
this.@originalContentLength = 0
this.@_linkUrl = null
this.@delegate = null
this.@originalLastMod = 0
this.@contentLength = 0
this.@declaringResource = null
this.@requestProcessors.clear()
this.@processed = false
attributes.entrySet().removeAll { it.key.startsWith(PROCESSED_BY_PREFIX) }
}
/**
* Calculate the URI of this resource relative to the base resource.
* All resource URLs must be app-relative with no ../ or ./
*/
String relativeTo(ResourceMeta base) {
if (actualAbsolute) {
return actualUrl
}
def baseDirStr = base.actualUrlParent
def thisDirStr = this.actualUrlParent
boolean isChild = thisDirStr.startsWith(baseDirStr+'/')
if (isChild) {
// Truncate to the part that is after the base dir
return this.actualUrl[baseDirStr.size()+1..-1]
} else {
def result = new StringBuilder()
def commonStem = new StringBuilder()
def baseUrl = base.actualUrl
// Eliminate the common portion - the base to which we need to ".."
def baseParts = baseUrl.tokenize('/')
def thisParts = actualUrl.tokenize('/')
int i = 0
for (; i < baseParts.size(); i++) {
if (thisParts[i] == baseParts[i]) {
commonStem << baseParts[i]+'/'
} else {
break;
}
}
if (baseParts.size()-1 > i) {
result << '../' * (baseParts.size()-1 - i)
}
result << actualUrl[commonStem.size()+1..-1]
return result.toString()
}
}
void updateActualUrlFromProcessedFile() {
def p = workDirRelativePath?.replace('\\', '/')
if (p != null) {
// have to call the setter method
setActualUrl(p)
} else {
setActualUrl(sourceUrl)
}
}
boolean excludesMapperOrOperation(String mapperName, String operationName) {
if (!excludedMappers) {
return false
}
def exclude = excludedMappers.contains("*")
if (!exclude) {
exclude = excludedMappers.contains(mapperName)
}
if (!exclude && operationName) {
exclude = excludedMappers.contains(operationName)
}
return exclude
}
void wasProcessedByMapper(ResourceMapper mapper, boolean processed = true) {
attributes[PROCESSED_BY_PREFIX+mapper.name] = processed
}
String toString() {
"ResourceMeta for URI ${sourceUrl} served by ${actualUrl} (delegate: ${delegating ? delegate : 'none'})"
}
}