Skip to content

Instantly share code, notes, and snippets.

@mirismaili
Last active November 24, 2018 05:32
Show Gist options
  • Save mirismaili/04144becb1f1d29f1e75914e3cd6540c to your computer and use it in GitHub Desktop.
Save mirismaili/04144becb1f1d29f1e75914e3cd6540c to your computer and use it in GitHub Desktop.
A wrapper for `java.nio.file.WatchService` that uses a hacky way to detect RENAME event instead of two sequential DELETE/CREATE events. Also, it uses callbacks to triggering events.

Extend WatchService class and override callbacks (onEntryCreated(), ...) to trigger related event.

There are two groups of callbacks:

  1. J group: onEntryCreatedJ(), onEntryModifiedJ(), onEntryDeletedJ(), onOverflowedJ():

    These are default (un-manipulated) callbacks. They are invoked regularly when a related event occurs. You should override them when you wish trigger native java.nio.file.WatchService events.

    For example override:

     onEntryCreatedJ(newFile:  File, watchEvent:  WatchEvent<*>)
    

    You can access to the name or path of the newly created file easily by newFile.name and newFile.toPath().

  2. Managed group: onEntryCreated(), onEntryModified(), onEntryDeleted(), onOverflowed() and onEntryRenamed():

    These are also invoked in response to the suitable event. But each series of this group, fires exactly after each series of the first group.

    E.g. if we have two sequential {CREATE -> MODIFY} event in one cycle of our main (outer) loop then first will fire {onEntryCreatedJ() -> onEntryModifiedJ()} (in first inner loop) and then will fire {onEntryCreated() -> onEntryModified()} (in second inner loop).

    NOW that you want comes: The program tries to each series of {DELETE -> CREATE} that is acutally is a {RENAME} and then replace them with that in the second group of callbacks. So simply override:

     onEntryRenamed(oldFile: File, newFile: File, renameEvent: RenameEvent)
    
package ir.openuniverse.watchservice
/**
* Created by [S. Mahdi Mir-Ismaili](https://mirismaili.github.io) on 1397/8/27 (18/11/2018).
*/
import com.google.gson.Gson
import org.apache.logging.log4j.LogManager
import java.io.Closeable
import java.lang.reflect.Field
inline fun <T> toJson(thing: T): String = Gson().toJson(thing)
val logger = LogManager.getLogger()!!
fun areEqual(obj1: Any?, obj2: Any?): Boolean {
if (obj1 == null) return obj2 == null
if (obj2 == null) return false
val theClass = obj1.javaClass
if (theClass !== obj2::class.java) return false //;logger.debug(theClass)
if (theClass.isPrimitive || obj1 is String || obj1 is Number || obj1 is Boolean ||
obj1 is Char || obj1 is Void)
return obj1 == obj2 //;logger.debug("X")
if (obj1 === obj2) return true
if (obj1 is Array<*>) {
obj2 as Array<*>
if (obj1.size != obj2.size) return false
for (i in 0 until obj1.size) if (!areEqual(obj1[i], obj2[i])) return false
return true
}
for (field in theClass.declaredFields)
if (!areEqual(safelyAccessToField(field, obj1).value, safelyAccessToField(field, obj2).value)) return false
return true
}
package ir.openuniverse.tinify
import java.io.File
import java.nio.file.Files
import java.nio.file.LinkOption.NOFOLLOW_LINKS
import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds.*
import java.nio.file.WatchEvent
import java.nio.file.attribute.BasicFileAttributeView
import java.nio.file.attribute.BasicFileAttributes
/**
* Created by [S. Mahdi Mir-Ismaili](https://mirismaili.github.io) on 1397/8/26 (17/11/2018).
*/
open class WatchService(private val directoryPath: String) {
private lateinit var deletionInformation: HashMap<String, DeletionInfo>
protected lateinit var pendingEvents: MutableList<WatchEvent<*>>
protected val files = HashMap<String, FileAttrs>()
fun watch() {
val folder = File(directoryPath)
val path = folder.toPath()
if (!folder.isDirectory) throw IllegalArgumentException("Path: '$path' is not a folder!")
val fileSystem = path.fileSystem
fileSystem.newWatchService().use { service: java.nio.file.WatchService ->
path.register(service, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE)
logger.debug("Watching path: $path ...")
for (file in folder.listFiles()) files[file.name] = getFileAttrs(file.toPath())
while (true) { // Start the infinite polling loop
val key = service.take() // This command will be blocked until an event occurs ...
deletionInformation = HashMap()
pendingEvents = mutableListOf()
for (watchEvent in key.pollEvents()) {
//logger.debug("...................")
pendingEvents.add(watchEvent)
val kind = watchEvent.kind()
val file = File("$path/${watchEvent.context()}")
logger.debug("$kind: [${file.name}]")
@Suppress("UNCHECKED_CAST")
when (kind) {
ENTRY_MODIFY -> {
onEntryModifiedJ(file, watchEvent as WatchEvent<Path>)
onEntryModified0(file, watchEvent)
}
ENTRY_CREATE -> {
onEntryCreatedJ(file, watchEvent as WatchEvent<Path>)
onEntryCreated0(file, watchEvent)
}
ENTRY_DELETE -> {
onEntryDeletedJ(file, watchEvent as WatchEvent<Path>)
onEntryDeleted0(file, watchEvent)
}
OVERFLOW -> onOverflowedJ(watchEvent)
}
}
for (watchEvent in pendingEvents) {
val kind = watchEvent.kind()
val file = File("$path/${watchEvent.context()}")
logger.debug("$kind: [${file.name}]")
@Suppress("UNCHECKED_CAST")
when (kind) {
ENTRY_MODIFY -> onEntryModified(file, watchEvent as WatchEvent<Path>)
ENTRY_CREATE -> onEntryCreated(file, watchEvent as WatchEvent<Path>)
ENTRY_DELETE -> onEntryDeleted(file, watchEvent as WatchEvent<Path>)
ENTRY_RENAME -> {
watchEvent as RenameEvent
onEntryRenamed(File("$path/${watchEvent.context0()}"), file, watchEvent)
}
OVERFLOW -> onOverflowed(watchEvent)
}
}
if (!key.reset()) break
logger.debug("------------------------")
}
}
}
protected open fun onEntryCreated0(newFile: File, watchEvent: WatchEvent<Path>) {
logger.debug(newFile.name)
val fileAttrs = getFileAttrs(newFile.toPath())
files[newFile.name] = fileAttrs
var oldFileName: String? = null
logger.trace(fileAttrs)
for ((name, deletionInfo) in deletionInformation)
if (deletionInfo.fileAttrs == fileAttrs) {
oldFileName = name
break
}
logger.debug("Old file name: $oldFileName")
if (oldFileName == null) return
val deleteEvent = deletionInformation[oldFileName]!!.watchEvent
//val delay = System.currentTimeMillis() - deletionInfo.deletionTime
// 1. Remove DeleteEvent:
pendingEvents.remove(deleteEvent)
// 2. Replace CreateEvent with RenameEvent:
pendingEvents[pendingEvents.indexOf(watchEvent)] = RenameEvent(deleteEvent.context() as Path, watchEvent.context() as Path)
deletionInformation.remove(oldFileName)
}
protected open fun onEntryModified0(file: File, watchEvent: WatchEvent<Path>) {
logger.debug(file.name)
files[file.name] = getFileAttrs(file.toPath())
}
protected open fun onEntryDeleted0(oldFile: File, watchEvent: WatchEvent<Path>) {
logger.debug(oldFile.name)
deletionInformation[oldFile.name] = DeletionInfo(files.remove(oldFile.name)!!, watchEvent, System.currentTimeMillis())
}
protected fun getFileAttrs(path: Path) = FileAttrs(Files.getFileAttributeView(
path, BasicFileAttributeView::class.java, NOFOLLOW_LINKS).readAttributes())
data class DeletionInfo(val fileAttrs: FileAttrs, val watchEvent: WatchEvent<*>, val deletionTime: Long)
companion object {
@Suppress("unused", "MemberVisibilityCanBePrivate")
class FileAttrs(attributes: BasicFileAttributes) {
val size = attributes.size()
val creationTime = attributes.creationTime().toMillis()
val lastModifiedTime = attributes.lastModifiedTime().toMillis()
val lastAccessTime = attributes.lastAccessTime().toMillis()
val isRegularFile = attributes.isRegularFile
val isDirectory = attributes.isDirectory
val isSymbolicLink = attributes.isSymbolicLink
val isOther = attributes.isOther
override fun equals(other: Any?): Boolean = areEqual(this, other)
override fun toString(): String = toJson(this)
override fun hashCode(): Int {
var result = size.hashCode()
result = 31 * result + creationTime.hashCode()
result = 31 * result + lastModifiedTime.hashCode()
result = 31 * result + lastAccessTime.hashCode()
result = 31 * result + isRegularFile.hashCode()
result = 31 * result + isDirectory.hashCode()
result = 31 * result + isSymbolicLink.hashCode()
result = 31 * result + isOther.hashCode()
return result
}
}
val ENTRY_RENAME = object : WatchEvent.Kind<Path> {
private val name = "ENTRY_RENAME"
override fun name(): String = name
override fun type(): Class<Path> = Path::class.java
override fun toString(): String = name
}
class RenameEvent(private val context0: Path, private val context1: Path) : Event(ENTRY_RENAME, context1) {
fun context0(): Path = context0
fun context1(): Path = context1
}
abstract class Event(private val kind: WatchEvent.Kind<Path>, private val context: Path, private var count: Int = 1) : WatchEvent<Path> {
override fun count(): Int = count
override fun kind(): WatchEvent.Kind<Path> = kind
override fun context(): Path = context
internal fun increment() = ++count
}
class CreateEvent(context: Path) : Event(ENTRY_CREATE, context)
class DeleteEvent(context: Path) : Event(ENTRY_DELETE, context)
class ModifyEvent(context: Path) : Event(ENTRY_MODIFY, context)
}
protected open fun onEntryCreated(newFile: File, watchEvent: WatchEvent<Path>) {}
protected open fun onEntryRenamed(oldFile: File, newFile: File, renameEvent: RenameEvent) {}
protected open fun onEntryModified(file: File, watchEvent: WatchEvent<Path>) {}
protected open fun onEntryDeleted(oldFile: File, watchEvent: WatchEvent<Path>) {}
protected open fun onOverflowed(watchEvent: WatchEvent<*>) {}
protected open fun onEntryCreatedJ(newFile: File, watchEvent: WatchEvent<Path>) = logger.trace(newFile.name)
protected open fun onEntryModifiedJ(file: File, watchEvent: WatchEvent<Path>) = logger.trace(file.name)
protected open fun onEntryDeletedJ(oldFile: File, watchEvent: WatchEvent<Path>) = logger.trace(oldFile.name)
protected open fun onOverflowedJ(watchEvent: WatchEvent<*>) {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment