-
Notifications
You must be signed in to change notification settings - Fork 103
/
TemplateEngine.scala
998 lines (865 loc) · 33.6 KB
/
TemplateEngine.scala
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
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
/**
* Copyright (C) 2009-2011 the original author or authors.
* See the notice.md file distributed with this work for additional
* information regarding copyright ownership.
*
* Licensed 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
*
* 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.fusesource.scalate
import java.io.{ File, PrintWriter, StringWriter }
import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
import org.fusesource.scalate.filter._
import org.fusesource.scalate.jade.JadeCodeGenerator
import org.fusesource.scalate.layout.{ LayoutStrategy, NullLayoutStrategy }
import org.fusesource.scalate.mustache.MustacheCodeGenerator
import org.fusesource.scalate.scaml.ScamlCodeGenerator
import org.fusesource.scalate.ssp.SspCodeGenerator
import org.fusesource.scalate.support._
import org.fusesource.scalate.util._
import scala.collection.immutable.TreeMap
import scala.collection.mutable.HashMap
import scala.compat.Platform
import scala.language.existentials
import scala.util.control.Exception
import scala.util.parsing.input.{ OffsetPosition, Position }
import scala.xml.NodeSeq
object TemplateEngine {
val log = Log(getClass)
def apply(sourceDirectories: Iterable[File], mode: String): TemplateEngine = {
new TemplateEngine(sourceDirectories, mode)
}
/**
* The default template types available in Scalate
*/
val templateTypes: List[String] = List("mustache", "ssp", "scaml", "jade")
}
/**
* A TemplateEngine is used to compile and load Scalate templates.
* The TemplateEngine takes care of setting up the Scala compiler
* and caching compiled templates for quicker subsequent loads
* of a requested template.
*
* The TemplateEngine uses a ''workingDirectory'' to store the generated scala source code and the bytecode. By default
* this uses a dynamically generated directory. You can configure this yourself to use whatever directory you wish.
* Or you can use the ''scalate.workdir'' system property to specify the workingDirectory
*
* @author <a href="http://hiramchirino.com">Hiram Chirino</a>
*/
class TemplateEngine(
var sourceDirectories: Iterable[File] = None,
var mode: String = System.getProperty("scalate.mode", "production")) {
import TemplateEngine.log._
private case class CacheEntry(
template: Template,
dependencies: Set[String],
timestamp: Long) {
def isStale() = timestamp != 0 && dependencies.exists {
resourceLoader.lastModified(_) > timestamp
}
}
/**
* Whether or not markup sensitive characters for HTML/XML elements like & > < are escaped or not
*/
var escapeMarkup = true
/**
* Set to false if you don't want the template engine to ever cache any of the compiled templates.
*
* If not explicitly configured this property can be configured using the ''scalate.allowCaching'' system property
*/
var allowCaching = "true" == System.getProperty("scalate.allowCaching", "true")
/**
* If true, then the template engine will check to see if the template has been updated since last compiled
* so that it can be reloaded. Defaults to true. YOu should set to false in production environments since
* the templates should not be changing.
*
* If not explicitly configured this property can be configured using the ''scalate.allowReload'' system property
*/
var allowReload = "true" == System.getProperty("scalate.allowReload", "true")
private[this] var compilerInstalled = true
/**
* Whether a custom classpath should be combined with the deduced classpath
*/
var combinedClassPath = false
/**
* Sets the import statements used in each generated template class
*/
var importStatements: List[String] = List(
"import _root_.scala.collection.JavaConverters._",
"import _root_.org.fusesource.scalate.support.TemplateConversions._",
"import _root_.org.fusesource.scalate.util.Measurements._")
/**
* Loads resources such as the templates based on URIs
*/
var resourceLoader: ResourceLoader = FileResourceLoader(sourceDirectoriesForwarder)
/**
* A list of directories which are searched to load requested templates.
*/
var templateDirectories = List("")
var packagePrefix = ""
var bootClassName = "scalate.Boot"
var bootInjections: List[AnyRef] = List(this)
private[this] val booted = new AtomicBoolean()
def boot(): Unit = {
if (booted.compareAndSet(false, true)) {
if (allowReload) {
// Is the Scala compiler on the class path?
try {
getClass.getClassLoader.loadClass("scala.tools.nsc.settings.ScalaSettings")
} catch {
case e: Throwable =>
// if it's not, then disable class reloading..
debug("Scala compiler not found on the class path. Template reloading disabled.")
allowReload = false
compilerInstalled = false
}
}
ClassLoaders.findClass(bootClassName, List(classLoader, Thread.currentThread.getContextClassLoader)) match {
case Some(clazz) =>
Boots.invokeBoot(clazz, bootInjections)
case _ =>
info("No bootstrap class " + bootClassName + " found on classloader: " + classLoader)
}
}
}
/**
* A forwarder so we can refer to whatever the current latest value of sourceDirectories is even if the value
* is mutated after the TemplateEngine is constructed
*/
protected def sourceDirectoriesForwarder = {
this.sourceDirectories
}
/**
* The supported template engines and their default extensions
*/
var codeGenerators: Map[String, CodeGenerator] = Map("ssp" -> new SspCodeGenerator, "scaml" -> new ScamlCodeGenerator,
"mustache" -> new MustacheCodeGenerator, "jade" -> new JadeCodeGenerator)
var filters: Map[String, Filter] = Map()
def filter(name: String) = codeGenerators.get(name).map(gen =>
new Filter() {
def filter(context: RenderContext, content: String) = {
context.capture(compileText(name, content))
}
}).orElse(filters.get(name))
var pipelines: Map[String, List[Filter]] = Map()
/**
* Maps file extensions to possible template extensions for custom mappins such as for
* Map("js" -> Set("coffee"), "css" => Set("sass", "scss"))
*/
var extensionToTemplateExtension: collection.mutable.Map[String, collection.mutable.Set[String]] = collection.mutable.Map()
/**
* Returns the mutable set of template extensions which are mapped to the given URI extension.
*/
def templateExtensionsFor(extension: String): collection.mutable.Set[String] = {
extensionToTemplateExtension.getOrElseUpdate(extension, collection.mutable.Set())
}
private[this] val attempt = Exception.ignoring(classOf[Throwable])
/**
* Returns the file extensions understood by Scalate; all the template engines and pipelines including
* the wiki markup languages.
*/
def extensions: Set[String] = (codeGenerators.keySet ++ pipelines.keySet).toSet
// Attempt to load all the built in filters.. Some may not load do to missing classpath
// dependencies.
attempt(filters += "plain" -> PlainFilter)
attempt(filters += "javascript" -> JavascriptFilter)
attempt(filters += "coffeescript" -> CoffeeScriptFilter)
attempt(filters += "css" -> CssFilter)
attempt(filters += "cdata" -> CdataFilter)
attempt(filters += "escaped" -> EscapedFilter)
attempt {
CoffeeScriptPipeline(this)
}
var layoutStrategy: LayoutStrategy = NullLayoutStrategy
lazy val compiler = createCompiler
var compilerInitialized = false
/**
* Factory method to create a compiler for this TemplateEngine.
* Override if you wish to contorl the compilation in a different way
* such as in side SBT or something.
*/
protected def createCompiler: Compiler = {
compilerInitialized = true
ScalaCompiler.create(this)
}
def shutdown() = if (compilerInitialized) compiler.shutdown
def sourceDirectory = new File(workingDirectory, "src")
def bytecodeDirectory = new File(workingDirectory, "classes")
def libraryDirectory = new File(workingDirectory, "lib")
def tmpDirectory = new File(workingDirectory, "tmp")
var classpath: String = null
private[this] var _workingDirectory: File = null
var classLoader = getClass().getClassLoader()
/**
* By default lets bind the context so we get to reuse its methods in a template
*/
var bindings = Binding("context", "_root_." + classOf[RenderContext].getName, true, None, "val", false) :: Nil
val finderCache = new ConcurrentHashMap[String, String]
private[this] val templateCache = new HashMap[String, CacheEntry]
private[this] var _cacheHits = 0
private[this] var _cacheMisses = 0
// Discover bits that can enhance the default template engine configuration. (like filters)
ClassFinder.discoverCommands[TemplateEngineAddOn]("META-INF/services/org.fusesource.scalate/addon.index").foreach { addOn =>
debug("Installing Scalate add on " + addOn.getClass)
addOn(this)
}
override def toString = getClass.getSimpleName + "(sourceDirectories: " + sourceDirectories + ")"
/**
* Returns true if this template engine is being used in development mode.
*/
def isDevelopmentMode = mode != null && mode.toLowerCase.startsWith("d")
/**
* If not explicitly configured this will default to using the ''scalate.workdir'' system property to specify the
* directory used for generating the scala source code and compiled bytecode - otherwise a temporary directory is used
*/
def workingDirectory: File = {
// Use a temp working directory if none is configured.
if (_workingDirectory == null) {
val value = System.getProperty("scalate.workdir", "")
if (value != null && value.length > 0) {
_workingDirectory = new File(value)
} else {
val f = File.createTempFile("scalate-", "-workdir")
// now lets delete the file so we can make a new directory there instead
f.delete
if (f.mkdirs) {
_workingDirectory = f
f.deleteOnExit
} else {
warn("Could not delete file %s so we could create a temp directory", f)
_workingDirectory = new File(new File(System.getProperty("java.io.tmpdir")), "_scalate")
}
}
}
_workingDirectory
}
def workingDirectory_=(value: File) = {
this._workingDirectory = value
}
/**
* Compiles the given Moustache template text and returns the template
*/
def compileMoustache(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
compileText("mustache", text, extraBindings)
}
/**
* Compiles the given SSP template text and returns the template
*/
def compileSsp(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
compileText("ssp", text, extraBindings)
}
/**
* Compiles the given SSP template text and returns the template
*/
def compileScaml(text: String, extraBindings: Iterable[Binding] = Nil): Template = {
compileText("scaml", text, extraBindings)
}
/**
* Compiles the given text using the given extension (such as ssp or scaml for example to denote what parser to use)
* and return the template
*/
def compileText(extension: String, text: String, extraBindings: Iterable[Binding] = Nil): Template = {
tmpDirectory.mkdirs()
val file = File.createTempFile("_scalate_tmp_", "." + extension, tmpDirectory)
IOUtil.writeText(file, text)
val loader = FileResourceLoader(List(tmpDirectory))
compile(TemplateSource.fromUri(file.getName, loader), extraBindings)
}
/**
* Compiles a template source without placing it in the template cache. Useful for temporary
* templates or dynamically created template
*/
def compile(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Template = {
compileAndLoad(source, extraBindings, 0)._1
}
/**
* Generates the Scala code for a template. Useful for generating scala code that
* will then be compiled into the application as part of a build process.
*/
def generateScala(source: TemplateSource, extraBindings: Iterable[Binding] = Nil) = {
source.engine = this
generator(source).generate(this, source, bindings ++ extraBindings)
}
/**
* Generates the Scala code for a template. Useful for generating scala code that
* will then be compiled into the application as part of a build process.
*/
def generateScala(uri: String, extraBindings: Iterable[Binding]): Code = {
generateScala(uriToSource(uri), extraBindings)
}
/**
* Generates the Scala code for a template. Useful for generating scala code that
* will then be compiled into the application as part of a build process.
*/
def generateScala(uri: String): Code = {
generateScala(uriToSource(uri))
}
/**
* The number of times a template load request was serviced from the cache.
*/
def cacheHits = templateCache.synchronized { _cacheHits }
/**
* The number of times a template load request could not be serviced from the cache
* and was loaded from disk.
*/
def cacheMisses = templateCache.synchronized { _cacheMisses }
/**
* Expire specific template source and then compile and cache again
*/
def expireAndCompile(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Unit = {
templateCache.synchronized {
templateCache.get(source.uri) match {
case None => {
sourceMapLog.info(s"${source.uri} not exist in cache just compiling")
cache(source, compileAndLoadEntry(source, extraBindings))
}
case Some(entry) => {
sourceMapLog.info(s"${source.uri} found in cache to expire")
templateCache.remove(source.uri)
cache(source, compileAndLoadEntry(source, extraBindings))
sourceMapLog.info(s"${source.uri} removed from cache and compiled")
}
}
}
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(
source: TemplateSource,
extraBindings: Iterable[Binding] = Nil): Template = {
source.engine = this
templateCache.synchronized {
// on the first load request, check to see if the INVALIDATE_CACHE JVM option is enabled
if (_cacheHits == 0 && _cacheMisses == 0 && java.lang.Boolean.getBoolean("org.fusesource.scalate.INVALIDATE_CACHE")) {
// this deletes generated scala and class files.
invalidateCachedTemplates
}
// Determine whether to build/rebuild the template, load existing .class files from the file system,
// or reuse an existing template that we've already loaded
templateCache.get(source.uri) match {
// Not in the cache..
case None =>
_cacheMisses += 1
try {
// Try to load a pre-compiled template from the classpath
cache(source, loadPrecompiledEntry(source, extraBindings))
} catch {
case _: Throwable =>
// It was not pre-compiled... compile and load it.
cache(source, compileAndLoadEntry(source, extraBindings))
}
// It was in the cache..
case Some(entry) =>
// check for staleness
if (allowReload && entry.isStale) {
// Cache entry is stale, re-compile it
_cacheMisses += 1
cache(source, compileAndLoadEntry(source, extraBindings))
} else {
// Cache entry is valid
_cacheHits += 1
entry.template
}
}
}
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(file: File, extraBindings: Iterable[Binding]): Template = {
load(TemplateSource.fromFile(file), extraBindings)
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(file: File): Template = {
load(TemplateSource.fromFile(file))
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(uri: String, extraBindings: Iterable[Binding]): Template = {
load(uriToSource(uri), extraBindings)
}
/**
* Compiles and then caches the specified template. If the template
* was previously cached, the previously compiled template instance
* is returned. The cache entry in invalidated and then template
* is re-compiled if the template file has been updated since
* it was last compiled.
*/
def load(uri: String): Template = {
load(uriToSource(uri))
}
/**
* Returns a template source for the given URI and current resourceLoader
*/
def source(uri: String): TemplateSource = TemplateSource.fromUri(uri, resourceLoader)
/**
* Returns a template source of the given type of template for the given URI and current resourceLoader
*/
def source(uri: String, templateType: String): TemplateSource = source(uri).templateType(templateType)
/**
* Returns true if the URI can be loaded as a template
*/
def canLoad(source: TemplateSource, extraBindings: Iterable[Binding] = Nil): Boolean = {
try {
load(source, extraBindings) != null
} catch {
case e: ResourceNotFoundException => false
}
}
/**
* Returns true if the URI can be loaded as a template
*/
def canLoad(uri: String): Boolean = {
canLoad(uriToSource(uri))
}
/**
* Returns true if the URI can be loaded as a template
*/
def canLoad(uri: String, extraBindings: Iterable[Binding]): Boolean = {
canLoad(uriToSource(uri), extraBindings)
}
/**
* Invalidates all cached Templates.
*/
def invalidateCachedTemplates() = {
templateCache.synchronized {
templateCache.clear
finderCache.clear
IOUtil.recursiveDelete(sourceDirectory)
IOUtil.recursiveDelete(bytecodeDirectory)
sourceDirectory.mkdirs
bytecodeDirectory.mkdirs
}
}
// Layout as text methods
//-------------------------------------------------------------------------
/**
* Renders the given template URI using the current layoutStrategy
*/
def layout(uri: String, context: RenderContext, extraBindings: Iterable[Binding]): Unit = {
val template = load(uri, extraBindings)
layout(template, context)
}
/**
* Renders the given template using the current layoutStrategy
*/
def layout(template: Template, context: RenderContext): Unit = {
RenderContext.using(context) {
val source = template.source
if (source != null && source.uri != null) {
context.withUri(source.uri) {
layoutStrategy.layout(template, context)
}
} else {
layoutStrategy.layout(template, context)
}
}
}
/**
* Renders the given template URI returning the output
*/
def layout(
uri: String,
attributes: Map[String, Any] = Map(),
extraBindings: Iterable[Binding] = Nil): String = {
val template = load(uri, extraBindings)
layout(uri, template, attributes)
}
def layout(
uri: String,
out: PrintWriter,
attributes: Map[String, Any]): Unit = {
val template = load(uri)
layout(uri, template, out, attributes)
}
protected def layout(
uri: String,
template: Template,
out: PrintWriter,
attributes: Map[String, Any]): Unit = {
val context = createRenderContext(uri, out)
for ((key, value) <- attributes) {
context.attributes(key) = value
}
layout(template, context)
}
/**
* Renders the given template returning the output
*/
def layout(
uri: String,
template: Template,
attributes: Map[String, Any]): String = {
val buffer = new StringWriter()
val out = new PrintWriter(buffer)
layout(uri, template, out, attributes)
buffer.toString
}
// can't use multiple methods with default arguments so lets manually expand them here...
def layout(uri: String, context: RenderContext): Unit = layout(uri, context, Nil)
def layout(uri: String, template: Template): String = layout(uri, template, Map[String, Any]())
/**
* Renders the given template source using the current layoutStrategy
*/
def layout(source: TemplateSource): String = layout(source, Map[String, Any]())
/**
* Renders the given template source using the current layoutStrategy
*/
def layout(source: TemplateSource, attributes: Map[String, Any]): String = {
val template = load(source)
layout(source.uri, template, attributes)
}
/**
* Renders the given template source using the current layoutStrategy
*/
def layout(
source: TemplateSource,
context: RenderContext,
extraBindings: Iterable[Binding]): Unit = {
val template = load(source, extraBindings)
layout(template, context)
}
/**
* Renders the given template source using the current layoutStrategy
*/
def layout(source: TemplateSource, context: RenderContext): Unit = {
val template = load(source)
layout(template, context)
}
// Layout as markup methods
//-------------------------------------------------------------------------
/**
* Renders the given template URI returning the output
*/
def layoutAsNodes(
uri: String,
attributes: Map[String, Any] = Map(),
extraBindings: Iterable[Binding] = Nil): NodeSeq = {
val template = load(uri, extraBindings)
layoutAsNodes(uri, template, attributes)
}
/**
* Renders the given template returning the output
*/
def layoutAsNodes(
uri: String,
template: Template,
attributes: Map[String, Any]): NodeSeq = {
// TODO there is a much better way of doing this by adding native NodeSeq
// support into the generated templates - especially for Scaml!
// for now lets do it a crappy way...
val buffer = new StringWriter()
val out = new PrintWriter(buffer)
val context = createRenderContext(uri, out)
for ((key, value) <- attributes) {
context.attributes(key) = value
}
//layout(template, context)
context.captureNodeSeq(template)
}
// can't use multiple methods with default arguments so lets manually expand them here...
def layoutAsNodes(uri: String, template: Template): NodeSeq = layoutAsNodes(uri, template, Map[String, Any]())
/**
* Factory method used by the layout helper methods that should be overloaded by template engine implementations
* if they wish to customize the render context implementation
*/
protected def createRenderContext(uri: String, out: PrintWriter): RenderContext = new DefaultRenderContext(uri, this, out)
private def loadPrecompiledEntry(
source: TemplateSource,
extraBindings: Iterable[Binding]) = {
source.engine = this
val className = source.className
val template = loadCompiledTemplate(className, allowCaching)
template.source = source
if (allowCaching && allowReload && resourceLoader.exists(source.uri)) {
// Even though the template was pre-compiled, it may go or is stale
// We still need to parse the template to figure out it's dependencies..
val code = generateScala(source, extraBindings)
val entry = CacheEntry(template, code.dependencies, lastModified(template.getClass))
if (entry.isStale) {
// Throw an exception since we should not load stale pre-compiled classes.
throw new StaleCacheEntryException(source)
}
// Yay the template is not stale. Lets use it.
entry
} else {
// If we are not going to be cache reloading.. then we
// don't need to do the extra work.
CacheEntry(template, Set(), 0)
}
}
private def compileAndLoadEntry(
source: TemplateSource,
extraBindings: Iterable[Binding]) = {
val (template, dependencies) = compileAndLoad(source, extraBindings, 0)
CacheEntry(template, dependencies, Platform.currentTime)
}
private def cache(source: TemplateSource, ce: CacheEntry): Template = {
if (allowCaching) {
templateCache += (source.uri -> ce)
}
val answer = ce.template
debug("Loaded uri: " + source.uri + " template: " + answer)
answer
}
/**
* Returns the source file of the template URI
*/
protected def sourceFileName(uri: String) = {
// Write the source code to file..
// to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
new File(sourceDirectory, uri.replace(':', '_') + ".scala")
}
protected def classFileName(uri: String) = {
// Write the source code to file..
// to avoid paths like foo/bar/C:/whatnot on windows lets mangle the ':' character
new File(sourceDirectory, uri.replace(':', '_') + ".scala")
}
protected val sourceMapLog = Log(getClass, "SourceMap")
private def compileAndLoad(
source: TemplateSource,
extraBindings: Iterable[Binding],
attempt: Int): (Template, Set[String]) = {
source.engine = this
var code: Code = null
try {
val uri = source.uri
// Can we use a pipeline to process the request?
pipeline(source) match {
case Some(p) =>
val template = new PipelineTemplate(p, source.text)
template.source = source
return (template, Set(uri))
case None =>
}
if (!compilerInstalled) {
throw new ResourceNotFoundException(
"Scala compiler not on the classpath. You must either add it to the classpath or precompile all the templates")
}
val g = generator(source)
// Generate the scala source code from the template
code = g.generate(this, source, bindings ++ extraBindings)
val sourceFile = sourceFileName(uri)
sourceFile.getParentFile.mkdirs
IOUtil.writeBinaryFile(sourceFile, code.source.getBytes("UTF-8"))
// Compile the generated scala code
compiler.compile(sourceFile)
// Write the source map information to the class file
val sourceMap = buildSourceMap(g.stratumName, uri, sourceFile, code.positions)
sourceMapLog.debug("installing:" + sourceMap)
storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/') + ".class"), sourceMap)
storeSourceMap(new File(bytecodeDirectory, code.className.replace('.', '/') + "$.class"), sourceMap)
// Load the compiled class and instantiate the template object
val template = loadCompiledTemplate(code.className)
template.source = source
(template, code.dependencies)
} catch {
// TODO: figure out why we sometimes get these InstantiationException errors that
// go away if you redo
case e: InstantiationException =>
if (attempt == 0) {
compileAndLoad(source, extraBindings, 1)
} else {
throw new TemplateException(e.getMessage, e)
}
case e: CompilerException =>
// Translate the scala error location info
// to the template locations..
def template_pos(pos: Position) = {
pos match {
case p: OffsetPosition => {
val filtered = code.positions.filterKeys(code.positions.ordering.compare(_, p) <= 0)
if (filtered.isEmpty) {
null
} else {
val (key, value) = filtered.last
// TODO: handle the case where the line is different too.
val colChange = pos.column - key.column
if (colChange >= 0) {
OffsetPosition(value.source, value.offset + colChange)
} else {
pos
}
}
}
case _ => null
}
}
var newmessage = "Compilation failed:\n"
val errors = e.errors.map {
(olderror) =>
val uri = source.uri
val pos = template_pos(olderror.pos)
if (pos == null) {
newmessage += ":" + olderror.pos + " " + olderror.message + "\n"
newmessage += olderror.pos.longString + "\n"
olderror
} else {
newmessage += uri + ":" + pos + " " + olderror.message + "\n"
newmessage += pos.longString + "\n"
// TODO should we pass the source?
CompilerError(uri, olderror.message, pos, olderror)
}
}
error(e)
if (e.errors.isEmpty) {
throw e
} else {
throw new CompilerException(newmessage, errors)
}
case e: InvalidSyntaxException =>
e.source = source
throw e
case e: TemplateException => throw e
case e: ResourceNotFoundException => throw e
case e: Throwable => throw new TemplateException(e.getMessage, e)
}
}
/**
* Gets a pipeline to use for the give uri string by looking up the uri's extension
* in the the pipelines map.
*/
protected def pipeline(source: TemplateSource): Option[List[Filter]] = {
//sort the extensions so we match the longest first.
for (ext <- pipelines.keys.toList.sortWith { case (x, y) => if (x.length == y.length) x.compareTo(y) < 0 else x.length > y.length } if source.uri.endsWith("." + ext)) {
return pipelines.get(ext)
}
None
}
/**
* Gets the code generator to use for the give uri string by looking up the uri's extension
* in the the codeGenerators map.
*/
protected def generator(source: TemplateSource): CodeGenerator = {
extension(source) match {
case Some(ext) =>
generatorForExtension(ext)
case None =>
throw new TemplateException("Template file extension missing. Cannot determine which template processor to use.")
}
}
/**
* Extracts the extension from the source's uri though derived engines could override this behaviour to
* auto default missing extensions or performing custom mappings etc.
*/
protected def extension(source: TemplateSource): Option[String] = source.templateType
/**
* Returns the code generator for the given file extension
*/
protected def generatorForExtension(extension: String) = codeGenerators.get(extension) match {
case None =>
val extensions = pipelines.keySet.toList ::: codeGenerators.keySet.toList
throw new TemplateException("Not a template file extension (" + extensions.mkString(" | ") + "), you requested: " + extension);
case Some(generator) => generator
}
private def loadCompiledTemplate(
className: String,
from_cache: Boolean = true): Template = {
val cl = if (from_cache) {
new URLClassLoader(Array(bytecodeDirectory.toURI.toURL), classLoader)
} else {
classLoader
}
val clazz = try {
cl.loadClass(className)
} catch {
case e: ClassNotFoundException =>
if (packagePrefix == "") {
throw e
} else {
// Try without the package prefix.
cl.loadClass(className.stripPrefix(packagePrefix).stripPrefix("."))
}
}
clazz.asInstanceOf[Class[Template]].newInstance
}
/**
* Figures out the modification time of the class.
*/
private def lastModified(clazz: Class[_]): Long = {
val codeSource = clazz.getProtectionDomain.getCodeSource
if (codeSource != null && codeSource.getLocation.getProtocol == "file") {
val location = new File(codeSource.getLocation.getPath)
if (location.isDirectory) {
val classFile = new File(location, clazz.getName.replace('.', '/') + ".class")
if (classFile.exists) {
return classFile.lastModified
}
} else {
// class is inside an archive.. just use the modification time of the jar
return location.lastModified
}
}
// Bail out
0
}
protected def buildSourceMap(
stratumName: String,
uri: String,
scalaFile: File,
positions: TreeMap[OffsetPosition, OffsetPosition]) = {
val shortName = uri.split("/").last
val longName = uri.stripPrefix("/")
val stratum: SourceMapStratum = new SourceMapStratum(stratumName)
val fileId = stratum.addFile(shortName, longName)
// build a map of input-line -> List( output-line )
var smap = new TreeMap[Int, List[Int]]()
positions.foreach {
case (out, in) =>
val outs = out.line :: smap.getOrElse(in.line, Nil)
smap += in.line -> outs
}
// sort the output lines..
smap = smap.transform { (x, y) => y.sortWith(_ < _) }
smap.foreach {
case (in, outs) =>
outs.foreach {
out =>
stratum.addLine(in, fileId, 1, out, 1)
}
}
stratum.optimize
val sourceMap: SourceMap = new SourceMap
sourceMap.setOutputFileName(scalaFile.getName)
sourceMap.addStratum(stratum, true)
sourceMap.toString
}
protected def storeSourceMap(classFile: File, sourceMap: String) = {
SourceMapInstaller.store(classFile, sourceMap)
}
/**
* Creates a [[org.fusesource.scalate.TemplateSource]] from a URI
*/
protected def uriToSource(uri: String) = TemplateSource.fromUri(uri, resourceLoader)
}