2828import java .util .HashSet ;
2929import java .util .List ;
3030import java .util .Set ;
31+ import java .util .concurrent .ArrayBlockingQueue ;
32+ import java .util .concurrent .Semaphore ;
3133import org .apache .maven .plugin .MojoExecutionException ;
3234import org .apache .maven .plugin .MojoFailureException ;
3335import org .apache .maven .plugins .annotations .Execute ;
4345import org .seedstack .maven .classloader .ReloadingClassLoader ;
4446import org .seedstack .maven .livereload .LRServer ;
4547import org .seedstack .maven .runnables .DefaultLauncherRunnable ;
46- import org .seedstack .maven .watcher .AggregatingFileChangeListener ;
4748import org .seedstack .maven .watcher .DirectoryWatcher ;
49+ import org .seedstack .maven .watcher .FileChangeListener ;
4850import org .seedstack .maven .watcher .FileEvent ;
4951
5052/**
@@ -57,7 +59,6 @@ public class WatchMojo extends AbstractExecutableMojo {
5759 public static final int LIVE_RELOAD_PORT = 35729 ;
5860 private DirectoryWatcher directoryWatcher ;
5961 private Thread watcherThread ;
60- private AggregatingFileChangeListener fileChangeListener ;
6162 private List <String > compileSourceRoots ;
6263 private ReloadingClassLoader reloadingClassLoader ;
6364 private DefaultLauncherRunnable launcherRunnable ;
@@ -66,9 +67,8 @@ public class WatchMojo extends AbstractExecutableMojo {
6667 @ Override
6768 public void execute () throws MojoExecutionException , MojoFailureException {
6869 this .compileSourceRoots = Collections .unmodifiableList (getProject ().getCompileSourceRoots ());
69- this .fileChangeListener = new WatchFileChangeListener ();
7070 try {
71- this .directoryWatcher = new DirectoryWatcher (getLog (), fileChangeListener );
71+ this .directoryWatcher = new DirectoryWatcher (getLog (), new WatchFileChangeListener () );
7272 } catch (IOException e ) {
7373 throw new MojoExecutionException ("Unable to create directory watcher" , e );
7474 }
@@ -92,7 +92,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
9292 Runtime .getRuntime ().addShutdownHook (new Thread ("watcher" ) {
9393 @ Override
9494 public void run () {
95- fileChangeListener .stop ();
9695 directoryWatcher .stop ();
9796 watcherThread .interrupt ();
9897 try {
@@ -135,32 +134,74 @@ public ReloadingClassLoader run() {
135134 return reloadingClassLoader ;
136135 }
137136
138- private class WatchFileChangeListener extends AggregatingFileChangeListener {
137+ private class WatchFileChangeListener implements FileChangeListener {
138+ private final Semaphore semaphore = new Semaphore (1 );
139+ private final ArrayBlockingQueue <FileEvent > pending = new ArrayBlockingQueue <>(10000 );
140+
139141 @ Override
140- public void onAggregatedChanges (Set <FileEvent > fileEvents ) {
141- try {
142- if (getLog ().isDebugEnabled ()) {
143- for (FileEvent fileEvent : fileEvents ) {
144- getLog ().debug (fileEvent .getKind ().name () + ": " + fileEvent .getFile ().getAbsolutePath ());
142+ public void onChange (Set <FileEvent > fileEvents ) {
143+ pending .addAll (fileEvents );
144+ while (!pending .isEmpty ()) {
145+ boolean permit = false ;
146+ try {
147+ permit = semaphore .tryAcquire ();
148+ if (permit ) {
149+ HashSet <FileEvent > fileEventsToProcess = new HashSet <>();
150+ pending .drainTo (fileEventsToProcess );
151+ refresh (fileEventsToProcess );
152+ } else {
153+ pending .addAll (fileEvents );
154+ }
155+ } finally {
156+ if (permit ) {
157+ semaphore .release ();
145158 }
146159 }
160+ }
161+ }
147162
163+ private void refresh (Set <FileEvent > fileEvents ) {
164+ try {
148165 Set <File > compiledFilesToRemove = new HashSet <>();
149166 Set <File > compiledFilesToUpdate = new HashSet <>();
150167
151- analyzeEvents (fileEvents , compiledFilesToRemove , compiledFilesToUpdate );
168+ boolean newFiles = analyzeEvents (fileEvents , compiledFilesToRemove , compiledFilesToUpdate );
169+
170+ if (newFiles || !compiledFilesToRemove .isEmpty () || !compiledFilesToUpdate .isEmpty ()) {
171+ getLog ().info ("Source changes detected" );
152172
153- if (!compiledFilesToRemove .isEmpty () || !compiledFilesToUpdate .isEmpty ()) {
154- reloadingClassLoader .invalidateClasses (
155- analyzeClasses (compiledFilesToRemove , compiledFilesToUpdate )
156- );
173+ try {
174+ // Invalidate classes from source files that are gone
175+ reloadingClassLoader .invalidateClasses (analyzeClasses (compiledFilesToRemove ));
176+ } catch (MojoExecutionException e ) {
177+ getLog ().info ("Cannot detect removed classes, invalidating all classes" );
178+ reloadingClassLoader .invalidateAllClasses ();
179+ }
180+
181+ // Remove compiled files for source files that are gone
157182 removeFiles (compiledFilesToRemove );
183+
184+ try {
185+ // Invalidate classes from source files that have changed
186+ reloadingClassLoader .invalidateClasses (analyzeClasses (compiledFilesToUpdate ));
187+ } catch (MojoExecutionException e ) {
188+ getLog ().info ("Cannot detect changed classes, invalidating all classes" );
189+ reloadingClassLoader .invalidateAllClasses ();
190+ }
191+
192+ // Recompile the sources
158193 recompile ();
194+
195+ // Refresh the app
159196 launcherRunnable .refresh ();
197+
198+ // Trigger LiveReload
160199 if (lrServer != null ) {
161200 getLog ().info ("Triggering LiveReload" );
162201 lrServer .notifyChange ("/" );
163202 }
203+
204+ getLog ().info ("Refresh complete" );
164205 }
165206 } catch (Exception e ) {
166207 Throwable toLog = e .getCause ();
@@ -172,29 +213,32 @@ public void onAggregatedChanges(Set<FileEvent> fileEvents) {
172213 }
173214 }
174215
175- private void analyzeEvents (Set <FileEvent > fileEvents , Set <File > compiledFilesToRemove ,
216+ private boolean analyzeEvents (Set <FileEvent > fileEvents , Set <File > compiledFilesToRemove ,
176217 Set <File > compiledFilesToUpdate ) throws MojoExecutionException {
218+ boolean newFiles = false ;
177219 for (FileEvent fileEvent : fileEvents ) {
178220 File changedFile = fileEvent .getFile ();
179221 if (!changedFile .isDirectory ()) {
180222 for (String compileSourceRoot : compileSourceRoots ) {
181223 try {
182- String changedFilePath = changedFile .getCanonicalPath ();
224+ String canonicalChangedFile = changedFile .getCanonicalPath ();
183225 String sourceRootPath = new File (compileSourceRoot ).getCanonicalPath ();
184- if (changedFilePath .startsWith (sourceRootPath + File .separator )
185- && changedFilePath .endsWith (".java" )) {
186- String compiledFile = changedFilePath .substring (sourceRootPath .length ());
187- compiledFile = compiledFile .replaceAll ("\\ .java$" , ".class" );
188- File fullCompiledFile = new File (getClassesDirectory (), compiledFile );
189- switch (fileEvent .getKind ()) {
190- case MODIFY :
191- compiledFilesToUpdate .add (fullCompiledFile );
192- break ;
193- case DELETE :
194- compiledFilesToRemove .add (fullCompiledFile );
195- break ;
196- default :
197- // nothing to do
226+ if (canonicalChangedFile .startsWith (sourceRootPath + File .separator )
227+ && canonicalChangedFile .endsWith (".java" )) {
228+ if (fileEvent .getKind () == FileEvent .Kind .CREATE ) {
229+ if (changedFile .length () > 0 ) {
230+ // ignore empty files (not relevant for app refresh)
231+ getLog ().info ("NEW: " + canonicalChangedFile );
232+ newFiles = true ;
233+ }
234+ } else if (fileEvent .getKind () == FileEvent .Kind .MODIFY ) {
235+ getLog ().info ("MODIFIED: " + canonicalChangedFile );
236+ compiledFilesToUpdate .add (resolveCompiledFile (sourceRootPath ,
237+ canonicalChangedFile ));
238+ } else if (fileEvent .getKind () == FileEvent .Kind .DELETE ) {
239+ getLog ().info ("DELETED: " + canonicalChangedFile );
240+ compiledFilesToRemove .add (resolveCompiledFile (sourceRootPath ,
241+ canonicalChangedFile ));
198242 }
199243 }
200244 } catch (IOException e ) {
@@ -204,34 +248,38 @@ private void analyzeEvents(Set<FileEvent> fileEvents, Set<File> compiledFilesToR
204248 }
205249 }
206250 }
251+ return newFiles ;
252+ }
253+
254+ private File resolveCompiledFile (String sourceRootPath , String changedFilePath ) {
255+ return new File (getClassesDirectory (), changedFilePath
256+ .substring (sourceRootPath .length ())
257+ .replaceAll ("\\ .java$" , ".class" ));
207258 }
208259
209260 private void removeFiles (Set <File > compiledFilesToRemove ) throws MojoExecutionException {
210261 for (File file : compiledFilesToRemove ) {
211- if (!file .delete ()) {
262+ if (file . exists () && !file .delete ()) {
212263 throw new MojoExecutionException ("Unable to remove compiled file " + file .getAbsolutePath ());
213264 }
214265 }
215266 }
216267
217- private Set <String > analyzeClasses (Set <File > compiledFilesToRemove , Set < File > compiledFilesToUpdate ) {
268+ private Set <String > analyzeClasses (Set <File > classFiles ) throws MojoExecutionException {
218269 Set <String > classNamesToInvalidate = new HashSet <>();
219- for (File file : compiledFilesToRemove ) {
220- classNamesToInvalidate .addAll (collectClassNames (file ));
221- }
222- for (File file : compiledFilesToUpdate ) {
270+ for (File file : classFiles ) {
223271 classNamesToInvalidate .addAll (collectClassNames (file ));
224272 }
225273 return classNamesToInvalidate ;
226274 }
227275
228- private Set <String > collectClassNames (File classFile ) {
276+ private Set <String > collectClassNames (File classFile ) throws MojoExecutionException {
229277 Set <String > classNames = new HashSet <>();
230278 try (FileInputStream is = new FileInputStream (classFile )) {
231279 ClassReader classReader = new ClassReader (is );
232280 classReader .accept (new ClassNameCollector (classNames ), 0 );
233281 } catch (IOException e ) {
234- getLog (). warn ( "Unable to parse class file " + classFile .getAbsolutePath ());
282+ throw new MojoExecutionException ( "Unable to analyze class file " + classFile .getAbsolutePath ());
235283 }
236284 return classNames ;
237285 }
0 commit comments