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.PartFilePartsProvider
import com.jessebrault.ssg.part.PartType
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.specialpage.GspSpecialPageRenderer
import com.jessebrault.ssg.specialpage.SpecialPageFileSpecialPagesProvider
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.TextFileTextsProvider
import com.jessebrault.ssg.text.TextType
import groovy.io.FileType
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Logger
import org.apache.logging.log4j.core.LoggerContext
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
@CommandLine.Command(
@ -32,6 +40,10 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
private static final Logger logger = LogManager.getLogger(StaticSiteGeneratorCli)
static void main(String[] args) {
System.exit(new CommandLine(StaticSiteGeneratorCli).execute(args))
}
static class LogLevel {
@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')
LogLevel logLevel
@CommandLine.Option(names = ['-w', '--watch'], description = 'Run in watch mode.')
boolean watch
@Override
Integer call() {
logger.traceEntry()
@ -110,6 +121,17 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
// Get ssg object
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
// Do each build
builds.each {
@ -131,4 +153,94 @@ class StaticSiteGeneratorCli implements Callable<Integer> {
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
// Get all texts, templates, parts, and specialPages
def texts = config.textProviders.collectMany { it.getTextFiles() }
def templates = config.templatesProviders.collectMany { it.getTemplates() }
def parts = config.partsProviders.collectMany { it.getParts() }
def specialPages = config.specialPagesProviders.collectMany { it.getSpecialPages() }
def texts = config.textProviders.collectMany { it.provide() }
def templates = config.templatesProviders.collectMany { it.provide() }
def parts = config.partsProviders.collectMany { it.provide() }
def specialPages = config.specialPagesProviders.collectMany { it.provide() }
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
import com.jessebrault.ssg.provider.WithWatchableDir
import com.jessebrault.ssg.util.FileNameHandler
import groovy.io.FileType
import groovy.transform.EqualsAndHashCode
import groovy.transform.NullCheck
import groovy.transform.TupleConstructor
import org.slf4j.Logger
import org.slf4j.LoggerFactory
@TupleConstructor(includeFields = true, defaults = false)
@NullCheck
@EqualsAndHashCode(includeFields = true)
class PartFilePartsProvider implements PartsProvider {
class PartFilePartsProvider implements PartsProvider, WithWatchableDir {
private static final Logger logger = LoggerFactory.getLogger(PartFilePartsProvider)
private final Collection<PartType> partTypes
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) {
partTypes.find {
it.ids.contains(new FileNameHandler(file).getExtension())
@ -25,7 +30,7 @@ class PartFilePartsProvider implements PartsProvider {
}
@Override
Collection<Part> getParts() {
Collection<Part> provide() {
if (!partsDir.isDirectory()) {
throw new IllegalArgumentException('partsDir must be a directory')
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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