Merge pull request #1 from JesseBrault0709/watch

Watch
This commit is contained in:
JesseBrault0709 2023-01-10 12:47:00 -06:00 committed by GitHub
commit aefd0bc661
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 198 additions and 48 deletions

View File

@ -4,6 +4,7 @@ import com.jessebrault.ssg.buildscript.GroovyBuildScriptRunner
import com.jessebrault.ssg.part.GspPartRenderer import com.jessebrault.ssg.part.GspPartRenderer
import com.jessebrault.ssg.part.PartFilePartsProvider import com.jessebrault.ssg.part.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
import com.jessebrault.ssg.specialpage.SpecialPageType import com.jessebrault.ssg.specialpage.SpecialPageType
@ -14,12 +15,19 @@ import com.jessebrault.ssg.text.MarkdownFrontMatterGetter
import com.jessebrault.ssg.text.MarkdownTextRenderer import com.jessebrault.ssg.text.MarkdownTextRenderer
import com.jessebrault.ssg.text.TextFileTextsProvider import com.jessebrault.ssg.text.TextFileTextsProvider
import com.jessebrault.ssg.text.TextType import com.jessebrault.ssg.text.TextType
import groovy.io.FileType
import org.apache.logging.log4j.Level import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.core.LoggerContext import org.apache.logging.log4j.core.LoggerContext
import picocli.CommandLine import picocli.CommandLine
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchEvent
import java.nio.file.WatchKey
import java.util.concurrent.Callable import java.util.concurrent.Callable
@CommandLine.Command( @CommandLine.Command(
@ -32,6 +40,10 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
private static final Logger logger = LogManager.getLogger(StaticSiteGeneratorCli) private static final Logger logger = LogManager.getLogger(StaticSiteGeneratorCli)
static void main(String[] args) {
System.exit(new CommandLine(StaticSiteGeneratorCli).execute(args))
}
static class LogLevel { static class LogLevel {
@CommandLine.Option(names = ['--info'], description = 'Log at INFO level.') @CommandLine.Option(names = ['--info'], description = 'Log at INFO level.')
@ -45,13 +57,12 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
} }
static void main(String[] args) {
System.exit(new CommandLine(StaticSiteGeneratorCli).execute(args))
}
@CommandLine.ArgGroup(exclusive = true, heading = 'Log Level') @CommandLine.ArgGroup(exclusive = true, heading = 'Log Level')
LogLevel logLevel LogLevel logLevel
@CommandLine.Option(names = ['-w', '--watch'], description = 'Run in watch mode.')
boolean watch
@Override @Override
Integer call() { Integer call() {
logger.traceEntry() logger.traceEntry()
@ -110,6 +121,17 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
// Get ssg object // Get ssg object
def ssg = new SimpleStaticSiteGenerator() def ssg = new SimpleStaticSiteGenerator()
if (this.watch) {
generate(builds, ssg)
watch(builds, ssg)
} else {
generate(builds, ssg)
}
}
private static Integer generate(Collection<Build> builds, StaticSiteGenerator ssg) {
logger.traceEntry('builds: {}, ssg: {}', builds, ssg)
def hadDiagnostics = false def hadDiagnostics = false
// Do each build // Do each build
builds.each { builds.each {
@ -131,4 +153,94 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
logger.traceExit(hadDiagnostics ? 1 : 0) logger.traceExit(hadDiagnostics ? 1 : 0)
} }
private static Integer watch(Collection<Build> builds, StaticSiteGenerator ssg) {
logger.traceEntry('builds: {}, ssg: {}', builds, ssg)
// Setup watchService and watchKeys
def watchService = FileSystems.getDefault().newWatchService()
Map<WatchKey, Path> watchKeys = [:]
// Our Closure to register a directory path
def registerPath = { Path path ->
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException('path must be a directory, given: ' + path)
}
logger.debug('registering dir with path: {}', path)
def watchKey = path.register(
watchService,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY
)
watchKeys[watchKey] = path
logger.debug('watchKeys: {}', watchKeys)
}
// Get all base watchableDirs
Collection<WithWatchableDir> watchableProviders = []
builds.each {
it.config.textProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.templatesProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.partsProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
it.config.specialPagesProviders.each {
if (it instanceof WithWatchableDir) {
watchableProviders << it
}
}
}
// register them and their child directories using the Closure above
watchableProviders.each {
def baseDirFile = it.watchableDir
registerPath(baseDirFile.toPath())
baseDirFile.eachFile(FileType.DIRECTORIES) {
registerPath(it.toPath())
}
}
//noinspection GroovyInfiniteLoopStatement
while (true) {
def watchKey = watchService.take()
def path = watchKeys[watchKey]
if (path == null) {
logger.warn('unexpected watchKey: {}', watchKey)
} else {
watchKey.pollEvents().each {
assert it instanceof WatchEvent<Path>
def childName = it.context()
def childPath = path.resolve(childName)
if (it.kind() == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(childPath)) {
registerPath(childPath)
} else if (Files.isRegularFile(childPath)) {
logger.debug('detected {} for regularFile with path {}', it.kind(), childPath)
def t = new Thread({
generate(builds, ssg)
})
t.setName('workerThread')
t.start()
}
}
}
def valid = watchKey.reset()
if (!valid) {
def removedPath = watchKeys.remove(watchKey)
logger.debug('removed path: {}', removedPath)
}
}
//noinspection GroovyUnreachableStatement
logger.traceExit(0)
}
} }

View File

@ -26,10 +26,10 @@ class SimpleStaticSiteGenerator implements StaticSiteGenerator {
def config = build.config def config = build.config
// Get all texts, templates, parts, and specialPages // Get all texts, templates, parts, and specialPages
def texts = config.textProviders.collectMany { it.getTextFiles() } def texts = config.textProviders.collectMany { it.provide() }
def templates = config.templatesProviders.collectMany { it.getTemplates() } def templates = config.templatesProviders.collectMany { it.provide() }
def parts = config.partsProviders.collectMany { it.getParts() } def parts = config.partsProviders.collectMany { it.provide() }
def specialPages = config.specialPagesProviders.collectMany { it.getSpecialPages() } def specialPages = config.specialPagesProviders.collectMany { it.provide() }
logger.debug('\n\ttexts: {}\n\ttemplates: {}\n\tparts: {}\n\tspecialPages: {}', texts, templates, parts, specialPages) logger.debug('\n\ttexts: {}\n\ttemplates: {}\n\tparts: {}\n\tspecialPages: {}', texts, templates, parts, specialPages)

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg
trait WatchableProvider {
File watchableDir
}

View File

@ -1,23 +1,28 @@
package com.jessebrault.ssg.part package com.jessebrault.ssg.part
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType import groovy.io.FileType
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck @NullCheck
@EqualsAndHashCode(includeFields = true) @EqualsAndHashCode(includeFields = true)
class PartFilePartsProvider implements PartsProvider { class PartFilePartsProvider implements PartsProvider, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(PartFilePartsProvider) private static final Logger logger = LoggerFactory.getLogger(PartFilePartsProvider)
private final Collection<PartType> partTypes private final Collection<PartType> partTypes
private final File partsDir private final File partsDir
PartFilePartsProvider(Collection<PartType> partTypes, File partsDir) {
this.partTypes = partTypes
this.partsDir = partsDir
this.watchableDir = this.partsDir
}
private PartType getPartType(File file) { private PartType getPartType(File file) {
partTypes.find { partTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension()) it.ids.contains(new FileNameHandler(file).getExtension())
@ -25,7 +30,7 @@ class PartFilePartsProvider implements PartsProvider {
} }
@Override @Override
Collection<Part> getParts() { Collection<Part> provide() {
if (!partsDir.isDirectory()) { if (!partsDir.isDirectory()) {
throw new IllegalArgumentException('partsDir must be a directory') throw new IllegalArgumentException('partsDir must be a directory')
} }

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.part package com.jessebrault.ssg.part
interface PartsProvider { import com.jessebrault.ssg.provider.Provider
Collection<Part> getParts()
interface PartsProvider extends Provider<Collection<Part>> {
Collection<PartType> getPartTypes() Collection<PartType> getPartTypes()
} }

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.provider
interface Provider<T> {
T provide()
}

View File

@ -0,0 +1,5 @@
package com.jessebrault.ssg.provider
trait WithWatchableDir {
File watchableDir
}

View File

@ -1,24 +1,29 @@
package com.jessebrault.ssg.specialpage package com.jessebrault.ssg.specialpage
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType import groovy.io.FileType
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck @NullCheck
@EqualsAndHashCode(includeFields = true) @EqualsAndHashCode(includeFields = true)
class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider { class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(SpecialPageFileSpecialPagesProvider) private static final Logger logger = LoggerFactory.getLogger(SpecialPageFileSpecialPagesProvider)
private final Collection<SpecialPageType> specialPageTypes private final Collection<SpecialPageType> specialPageTypes
private final File specialPagesDir private final File specialPagesDir
SpecialPageFileSpecialPagesProvider(Collection<SpecialPageType> specialPageTypes, File specialPagesDir) {
this.specialPageTypes = specialPageTypes
this.specialPagesDir = specialPagesDir
this.watchableDir = this.specialPagesDir
}
private SpecialPageType getSpecialPageType(File file) { private SpecialPageType getSpecialPageType(File file) {
this.specialPageTypes.find { this.specialPageTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension()) it.ids.contains(new FileNameHandler(file).getExtension())
@ -26,7 +31,7 @@ class SpecialPageFileSpecialPagesProvider implements SpecialPagesProvider {
} }
@Override @Override
Collection<SpecialPage> getSpecialPages() { Collection<SpecialPage> provide() {
if (!this.specialPagesDir.isDirectory()) { if (!this.specialPagesDir.isDirectory()) {
throw new IllegalArgumentException('specialPagesDir must be a directory') throw new IllegalArgumentException('specialPagesDir must be a directory')
} }

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.specialpage package com.jessebrault.ssg.specialpage
interface SpecialPagesProvider { import com.jessebrault.ssg.provider.Provider
Collection<SpecialPage> getSpecialPages()
interface SpecialPagesProvider extends Provider<Collection<SpecialPage>> {
Collection<SpecialPageType> getSpecialPageTypes() Collection<SpecialPageType> getSpecialPageTypes()
} }

View File

@ -1,23 +1,28 @@
package com.jessebrault.ssg.template package com.jessebrault.ssg.template
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType import groovy.io.FileType
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck @NullCheck
@EqualsAndHashCode(includeFields = true) @EqualsAndHashCode(includeFields = true)
class TemplateFileTemplatesProvider implements TemplatesProvider { class TemplateFileTemplatesProvider implements TemplatesProvider, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(TemplateFileTemplatesProvider) private static final Logger logger = LoggerFactory.getLogger(TemplateFileTemplatesProvider)
private final Collection<TemplateType> templateTypes private final Collection<TemplateType> templateTypes
private final File templatesDir private final File templatesDir
TemplateFileTemplatesProvider(Collection<TemplateType> templateTypes, File templatesDir) {
this.templateTypes = templateTypes
this.templatesDir = templatesDir
this.watchableDir = this.templatesDir
}
private TemplateType getType(File file) { private TemplateType getType(File file) {
this.templateTypes.find { this.templateTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension()) it.ids.contains(new FileNameHandler(file).getExtension())
@ -25,7 +30,7 @@ class TemplateFileTemplatesProvider implements TemplatesProvider {
} }
@Override @Override
Collection<Template> getTemplates() { Collection<Template> provide() {
if (!this.templatesDir.isDirectory()) { if (!this.templatesDir.isDirectory()) {
throw new IllegalArgumentException('templatesDir must be a directory') throw new IllegalArgumentException('templatesDir must be a directory')
} }

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.template package com.jessebrault.ssg.template
interface TemplatesProvider { import com.jessebrault.ssg.provider.Provider
Collection<Template> getTemplates()
interface TemplatesProvider extends Provider<Collection<Template>> {
Collection<TemplateType> getTemplateTypes() Collection<TemplateType> getTemplateTypes()
} }

View File

@ -1,24 +1,32 @@
package com.jessebrault.ssg.text package com.jessebrault.ssg.text
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler import com.jessebrault.ssg.util.FileNameHandler
import com.jessebrault.ssg.util.RelativePathHandler import com.jessebrault.ssg.util.RelativePathHandler
import groovy.io.FileType import groovy.io.FileType
import groovy.transform.EqualsAndHashCode import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck @NullCheck
@EqualsAndHashCode(includeFields = true) @EqualsAndHashCode(includeFields = true)
class TextFileTextsProvider implements TextsProvider { class TextFileTextsProvider implements TextsProvider, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(TextFileTextsProvider) private static final Logger logger = LoggerFactory.getLogger(TextFileTextsProvider)
private final Collection<TextType> textTypes private final Collection<TextType> textTypes
private final File textsDir private final File textsDir
TextFileTextsProvider(Collection<TextType> textTypes, File textsDir) {
this.textTypes = textTypes
this.textsDir = textsDir
if (!this.textsDir.isDirectory()) {
throw new IllegalArgumentException('textsDir must be a directory, given: ' + this.textsDir)
}
this.watchableDir = this.textsDir
}
private TextType getTextType(File file) { private TextType getTextType(File file) {
this.textTypes.find { this.textTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension()) it.ids.contains(new FileNameHandler(file).getExtension())
@ -26,11 +34,7 @@ class TextFileTextsProvider implements TextsProvider {
} }
@Override @Override
Collection<Text> getTextFiles() { Collection<Text> provide() {
if (!this.textsDir.isDirectory()) {
throw new IllegalArgumentException('textsDir must be a directory')
}
def textFiles = [] def textFiles = []
this.textsDir.eachFileRecurse(FileType.FILES) { this.textsDir.eachFileRecurse(FileType.FILES) {
def type = this.getTextType(it) def type = this.getTextType(it)

View File

@ -1,6 +1,7 @@
package com.jessebrault.ssg.text package com.jessebrault.ssg.text
interface TextsProvider { import com.jessebrault.ssg.provider.Provider
Collection<Text> getTextFiles()
interface TextsProvider extends Provider<Collection<Text>> {
Collection<TextType> getTextTypes() Collection<TextType> getTextTypes()
} }

View File

@ -24,7 +24,7 @@ class PartFilePartsProviderTests {
write('Hello <%= name %>!') write('Hello <%= name %>!')
} }
def r = this.partsProvider.getParts() def r = this.partsProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def p0 = r[0] def p0 = r[0]
assertEquals('testPart.gsp', p0.path) assertEquals('testPart.gsp', p0.path)
@ -40,7 +40,7 @@ class PartFilePartsProviderTests {
} }
} }
def r = this.partsProvider.getParts() def r = this.partsProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def p0 = r[0] def p0 = r[0]
assertEquals('nested/testPart.gsp', p0.path) assertEquals('nested/testPart.gsp', p0.path)
@ -54,7 +54,7 @@ class PartFilePartsProviderTests {
write 'Ignored!' write 'Ignored!'
} }
def r = this.partsProvider.getParts() def r = this.partsProvider.provide()
assertEquals(0, r.size()) assertEquals(0, r.size())
} }

View File

@ -23,7 +23,7 @@ class SpecialPageFileSpecialPagesProviderTests {
new FileTreeBuilder(this.specialPagesDir) new FileTreeBuilder(this.specialPagesDir)
.file('test.gsp', '<%= "Hello, World!" %>') .file('test.gsp', '<%= "Hello, World!" %>')
def r = this.specialPagesProvider.getSpecialPages() def r = this.specialPagesProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def f0 = r[0] def f0 = r[0]
assertEquals('test', f0.path) assertEquals('test', f0.path)
@ -37,7 +37,7 @@ class SpecialPageFileSpecialPagesProviderTests {
file('nested.gsp', '<%= "Hello, World!" %>') file('nested.gsp', '<%= "Hello, World!" %>')
} }
def r = this.specialPagesProvider.getSpecialPages() def r = this.specialPagesProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def f0 = r[0] def f0 = r[0]
assertEquals('nested/nested', f0.path) assertEquals('nested/nested', f0.path)
@ -49,7 +49,7 @@ class SpecialPageFileSpecialPagesProviderTests {
void ignoresUnsupportedFile() { void ignoresUnsupportedFile() {
new FileTreeBuilder(this.specialPagesDir).file('.ignored', 'Ignored!') new FileTreeBuilder(this.specialPagesDir).file('.ignored', 'Ignored!')
def r = this.specialPagesProvider.getSpecialPages() def r = this.specialPagesProvider.provide()
assertEquals(0, r.size()) assertEquals(0, r.size())
} }

View File

@ -22,7 +22,7 @@ class PageTemplatesProviderTests {
void findsTemplate() { void findsTemplate() {
new File(this.templatesDir, 'test.gsp').write('<% out << text %>') new File(this.templatesDir, 'test.gsp').write('<% out << text %>')
def r = this.templatesProvider.getTemplates() def r = this.templatesProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def t0 = r[0] def t0 = r[0]
assertEquals('test.gsp', t0.path) assertEquals('test.gsp', t0.path)
@ -38,7 +38,7 @@ class PageTemplatesProviderTests {
} }
} }
def r = this.templatesProvider.getTemplates() def r = this.templatesProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def t0 = r[0] def t0 = r[0]
assertEquals('nested/nested.gsp', t0.path) assertEquals('nested/nested.gsp', t0.path)
@ -52,7 +52,7 @@ class PageTemplatesProviderTests {
write('Ignored!') write('Ignored!')
} }
def r = this.templatesProvider.getTemplates() def r = this.templatesProvider.provide()
assertEquals(0, r.size()) assertEquals(0, r.size())
} }

View File

@ -22,7 +22,7 @@ class TextFileTextsProviderTests {
void findsFile() { void findsFile() {
new FileTreeBuilder(this.textsDir).file('test.md', '**Hello, World!**') new FileTreeBuilder(this.textsDir).file('test.md', '**Hello, World!**')
def r = this.textsProvider.getTextFiles() def r = this.textsProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def f0 = r[0] def f0 = r[0]
assertEquals('test', f0.path) assertEquals('test', f0.path)
@ -36,7 +36,7 @@ class TextFileTextsProviderTests {
file('nested.md', '**Hello!**') file('nested.md', '**Hello!**')
} }
def r = this.textsProvider.getTextFiles() def r = this.textsProvider.provide()
assertEquals(1, r.size()) assertEquals(1, r.size())
def f0 = r[0] def f0 = r[0]
assertEquals('nested/nested', f0.path) assertEquals('nested/nested', f0.path)
@ -48,7 +48,7 @@ class TextFileTextsProviderTests {
void ignoresUnsupportedFile() { void ignoresUnsupportedFile() {
new FileTreeBuilder(this.textsDir).file('.ignored', 'Ignored!') new FileTreeBuilder(this.textsDir).file('.ignored', 'Ignored!')
def r = this.textsProvider.getTextFiles() def r = this.textsProvider.provide()
assertEquals(0, r.size()) assertEquals(0, r.size())
} }