-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathCombine.cfc
337 lines (261 loc) · 13.4 KB
/
Combine.cfc
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
<cfcomponent displayname="Combine" output="false" hint="provides javascript and css file merge and compress functionality, to reduce the overhead caused by file sizes & multiple requests">
<cffunction name="init" access="public" returntype="Combine" output="false">
<cfargument name="enableSCache" type="boolean" required="true" />
<cfargument name="cachePath" type="string" required="true" />
<cfargument name="enableCCache" type="boolean" required="true" />
<cfargument name="compressJS" type="boolean" required="true" hint="compress JavaScript?" />
<cfargument name="compressCSS" type="boolean" required="true" hint="compress CSS?" />
<!--- optional args --->
<cfargument name="outputSeperator" type="string" required="false" default="#chr(13)#" hint="seperates the output of different file content" />
<cfargument name="skipMissingFiles" type="boolean" required="false" default="true" hint="skip files that don't exists? If false, non-existent files will cause an error" />
<cfargument name="getFileModifiedMethod" type="string" required="false" default="java" hint="java or com. Which technique to use to get the last modified times for files." />
<cfscript>
variables.sCachePath = arguments.cachePath;
// enable server-side caching
variables.bSCache = arguments.enableSCache;
// enable client-side cacheing via etags and last-modified headers
variables.bCCache = arguments.enableCCache;
// enable compression of javascript
variables.bCompressJS = arguments.compressJS;
// enable css compression
variables.bCompressCss = arguments.compressCSS;
// text used to delimit the merged files in the final output
variables.sOutputDelimiter = arguments.outputSeperator;
// skip files that don't exists? If false, non-existent files will cause an error
variables.bSkipMissingFiles = arguments.skipMissingFiles;
// -----------------------------------------------------------------------
variables.jOutputStream = createObject("java","java.io.ByteArrayOutputStream");
variables.jStringReader = createObject("java","java.io.StringReader");
variables.jStringWriter = createObject("java","java.io.StringWriter");
// determine which method to use for getting the file last modified dates
if(arguments.getFileModifiedMethod eq 'com') {
variables.fso = CreateObject("COM", "Scripting.FileSystemObject");
// calls to getFileDateLastModified() are handled by getFileDateLastModified_com()
variables.getFileDateLastModified = variables.getFileDateLastModified_com;
} else {
variables.jFile = CreateObject("java", "java.io.File");
// calls to getFileDateLastModified() are handled by getFileDateLastModified_java()
variables.getFileDateLastModified = variables.getFileDateLastModified_java;
}
</cfscript>
<!--- Ensure server-side cache directory exists --->
<cfif variables.bSCache and not DirectoryExists(variables.sCachePath)>
<cfdirectory action="create" directory="#variables.sCachePath#" />
</cfif>
<cfreturn this />
</cffunction>
<cffunction name="combine" access="public" returntype="void" output="true" hint="combines a list js or css files into a single file, which is output, and cached if caching is enabled">
<cfargument name="arguments" type="struct" required="true" hint="Structure containing arguments" />
<cfscript>
var sType = '';
var lastModified = 0;
var lastModifiedDTO = '';
var sFilePath = '';
var sCorrectedFilePaths = '';
var i = 0;
var sDelimiter = '';
var etag = '';
var sCacheFile = '';
var sOutput = '';
var sFileContent = '';
var bIsCompressed = '';
var filePaths = '';
</cfscript>
<cfparam name="arguments.files" type="string" />
<cfparam name="arguments.delimiter" type="string" default="," />
<cfparam name="arguments.bSCache" type="boolean" default="#variables.bSCache#" />
<cfparam name="arguments.bCCache" type="boolean" default="#variables.bCCache#" />
<cfparam name="arguments.bCompressJS" type="boolean" default="#variables.bCompressJS#" />
<cfparam name="arguments.bCompressCSS" type="boolean" default="#variables.bCompressCSS#" />
<cfparam name="arguments.bSkipMissingFiles" type="boolean" default="#variables.bSkipMissingFiles#" />
<cfscript>
sDelimiter = arguments.delimiter;
filePaths = convertToAbsolutePaths(files, sDelimiter);
// determine what file type we are dealing with
sType = listLast( listFirst(filePaths, sDelimiter) , '.');
if (not listFindNoCase('js,css', sType)) {
throw("combine.invalidFileType", "Only JavaScript and CSS files can be combined.");
}
// determine if output should be compressed
bIsCompressed = IIf(sType eq 'js', arguments.bCompressJS, arguments.bCompressCSS);
</cfscript>
<!--- get the latest last modified date --->
<cfset sCorrectedFilePaths = '' />
<cfloop from="1" to="#listLen(filePaths, sDelimiter)#" index="i">
<cfset sFilePath = listGetAt(filePaths, i, sDelimiter) />
<cfif fileExists( sFilePath )>
<cfset lastModified = max(lastModified, getFileDateLastModified( sFilePath )) />
<cfset sCorrectedFilePaths = listAppend(sCorrectedFilePaths, sFilePath, sDelimiter) />
<cfelseif not arguments.bSkipMissingFiles>
<cfthrow type="combine.missingFileException" message="A file specified in the combine (#sType#) path doesn't exist." detail="file: #sFilePath#" extendedinfo="full combine path list: #filePaths#" />
</cfif>
</cfloop>
<cfset filePaths = sCorrectedFilePaths />
<!--- create a string to be used as an Etag - in the response header --->
<cfset etag = lastModified & '-' & hash(filePaths & bIsCompressed) />
<!--- Convert Unix epoch timestamp to ColdFusion date/time object --->
<cfset lastModifiedDTO = DateAdd("s", lastModified / 1000, DateConvert("utc2Local", "January 1 1970 00:00")) />
<!---
output the cache headers, this allows the browser to make conditional requests
(i.e. browser says to server: only return me the file if your eTag is different to mine)
--->
<cfif arguments.bCCache>
<cfheader name="ETag" value="""#etag#""" />
<cfheader name="Last-Modified" value="#GetHTTPTimeString(lastModifiedDTO)#" />
</cfif>
<!---
if the browser is doing a conditional request, then only send it the file if the browser's
etag doesn't match the server's etag (i.e. the browser's file is different to the server's)
--->
<cfif arguments.bCCache and not structKeyExists(url, 'reinit') and ((structKeyExists(cgi, 'HTTP_IF_NONE_MATCH') and cgi.HTTP_IF_NONE_MATCH contains eTag) or
(structKeyExists(GetHttpRequestData().headers, 'If-Modified-Since') and lastModifiedDTO lte DateConvert("utc2local", ParseDateTime(GetHttpRequestData().headers["If-Modified-Since"])) )) >
<!--- nothing has changed, return nothing --->
<cfheader statuscode="304" statustext="Not Modified" />
<!--- Seems to cause problems with IE and last-modified
<cfheader name="Content-Length" value="0" /> --->
<cfreturn />
<cfelse>
<!--- first time visit, or files have changed --->
<cfset sCacheFile = variables.sCachePath & '\' & etag & '.' & sType />
<cfif arguments.bSCache and not structKeyExists(url, 'reinit')>
<!--- try to return a cached version of the file --->
<cfif fileExists(sCacheFile)>
<cffile action="read" file="#sCacheFile#" variable="sOutput" />
<!--- output contents --->
<cfset outputContent(sOutput, sType) />
<cfreturn />
</cfif>
</cfif>
<!--- combine the file contents into 1 string --->
<cfset sOutput = '' />
<cfloop from="1" to="#listLen(filePaths, sDelimiter)#" index="i">
<cfset sFilePath = listGetAt(filePaths, i, sDelimiter) />
<cfif not listFindNoCase('js,css', listLast(sFilePath, '.'))>
<cfthrow type="combine.invalidFileType" message="Only JavaScript and CSS files can be combined." />
</cfif>
<cffile action="read" variable="sFileContent" file="#sFilePath#" />
<cfset sOutput = sOutput & variables.sOutputDelimiter & sFileContent />
</cfloop>
<cfscript>
// Compress the javascript and CSS if requested
if (sType eq 'js' and bIsCompressed) {
sOutput = compressJsWithYUI(sOutput);
} else if(sType eq 'css' and bIsCompressed) {
sOutput = compressCssWithYUI(sOutput);
}
//output contents
outputContent(sOutput, sType);
</cfscript>
<!--- write the cache file --->
<cfif arguments.bSCache>
<cffile action="write" file="#sCacheFile#" output="#sOutput#" />
</cfif>
</cfif>
</cffunction>
<cffunction name="outputContent" access="private" returnType="void" output="true">
<cfargument name="sOut" type="string" required="true" />
<cfargument name="sType" type="string" required="true" />
<cfset var mimeType = '' />
<cfif arguments.sType is "js">
<cfset mimeType = "application/javascript" />
<cfelseif arguments.sType is "css">
<cfset mimeType = "text/css" />
</cfif>
<!--- <cfheader name="Content-Length" value="#Len(arguments.sOut)#" /> --->
<cfcontent type="#mimeType#" />
<cfoutput>#arguments.sOut#</cfoutput>
</cffunction>
<!--- uses 'Scripting.FileSystemObject' com object --->
<cffunction name="getFileDateLastModified_com" access="private" returnType="string">
<cfargument name="path" type="string" required="true" />
<cfset var file = variables.fso.GetFile(arguments.path) />
<cfreturn file.DateLastModified />
</cffunction>
<!--- uses 'java.io.file'. Recommended --->
<cffunction name="getFileDateLastModified_java" access="private" returnType="string">
<cfargument name="path" type="string" required="true" />
<cfset var file = variables.jFile.init(arguments.path) />
<cfreturn file.lastModified() />
</cffunction>
<cffunction name="compressJsWithJSMin" access="private" returnType="string" hint="takes a javascript string and returns a compressed version, using JSMin">
<cfargument name="sInput" type="string" required="true" />
<cfscript>
var sOut = arguments.sInput;
var joOutput = '';
var joInput = '';
var joJSMin = '';
if (not structKeyExists(variables, "jJSMin")) {
variables.jJSMin = createObject("java","com.magnoliabox.jsmin.JSMin");
}
joOutput = variables.jOutputStream.init();
joInput = variables.jStringReader.init(sOut);
joJSMin = variables.jJSMin.init(joInput, joOutput);
joJSMin.jsmin();
joInput.close();
sOut = joOutput.toString();
joOutput.close();
return sOut;
</cfscript>
</cffunction>
<cffunction name="compressJsWithYUI" access="private" returnType="string" hint="takes a javascript string and returns a compressed version, using the YUI javascript compressor">
<cfargument name="sInput" type="string" required="true" />
<cfscript>
var sOut = arguments.sInput;
var joInput = '';
var joOutput = '';
var joErrorReporter = '';
var joYUI = '';
if (not structKeyExists(variables, "jYuiJavaScriptCompressor")) {
variables.jYuiJavaScriptCompressor = createObject("java","com.yahoo.platform.yui.compressor.JavaScriptCompressor");
variables.jErrorReporter = createObject("java","org.mozilla.javascript.ErrorReporter");
}
joInput = variables.jStringReader.init(sOut);
joOutput = variables.jStringWriter.init();
joErrorReporter = variables.jErrorReporter;
joYUI = variables.jYuiJavaScriptCompressor.init(joInput, joErrorReporter);
// compress(out, linebreak, munge, verbose, preserveAllSemiColons, disableOptimizations)
joYUI.compress(joOutput, javaCast('int',-1), javaCast('boolean', true), javaCast('boolean', false), javaCast('boolean', true), javaCast('boolean', false));
joInput.close();
sOut = joOutput.toString();
joOutput.close();
return sOut;
</cfscript>
</cffunction>
<cffunction name="compressCssWithYUI" access="private" returnType="string" hint="takes a css string and returns a compressed version, using the YUI css compressor">
<cfargument name="sInput" type="string" required="true" />
<cfscript>
var sOut = arguments.sInput;
var joInput = '';
var joOutput = '';
var joYUI = '';
if(not structKeyExists(variables, "jYuiCssCompressor")) {
variables.jYuiCssCompressor = createObject("java","com.yahoo.platform.yui.compressor.CssCompressor");
}
joInput = variables.jStringReader.init(sOut);
joOutput = variables.jStringWriter.init();
joYUI = variables.jYuiCssCompressor.init(joInput);
joYUI.compress(joOutput, javaCast('int',-1));
joInput.close();
sOut = joOutput.toString();
joOutput.close();
return sOut;
</cfscript>
</cffunction>
<cffunction name="convertToAbsolutePaths" access="private" returnType="string"output="false" hint="takes a list of relative paths and makes them absolute, using expandPath">
<cfargument name="relativePaths" type="string" required="true" hint="delimited list of relative paths" />
<cfargument name="delimiter" type="string" required="false" default="," hint="the delimiter used in the provided paths string" />
<cfset var filePaths = '' />
<cfset var path = '' />
<cfloop list="#arguments.relativePaths#" delimiters="#arguments.delimiter#" index="path">
<cfset filePaths = listAppend(filePaths, expandPath(path), arguments.delimiter) />
</cfloop>
<cfreturn filePaths />
</cffunction>
<cffunction name="throw" returntype="void" output="no" access="public">
<cfargument name="exceptionType" type="string" required="true" />
<cfargument name="message" type="string" required="false" default="" />
<cfargument name="detail" type="string" required="false" default="" />
<cfargument name="extendedInfo" type="string" required="false" default="" />
<cfthrow type="#Arguments.exceptionType#" message="#Arguments.message#" detail="#Arguments.detail#" extendedinfo="#Arguments.extendedInfo#" />
</cffunction>
</cfcomponent>