Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019-2020 Red Hat Inc. and others.
* Copyright (c) 2019-2026 Red Hat Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -15,7 +15,10 @@

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

Expand Down Expand Up @@ -61,6 +64,9 @@ public class MicroProfilePropertiesListenerManager {

private static final MicroProfilePropertiesListenerManager INSTANCE = new MicroProfilePropertiesListenerManager();

// Debounce delay to group multiple file changes into a single notification
private static final long DEBOUNCE_DELAY_MS = 2000;

public static MicroProfilePropertiesListenerManager getInstance() {
return INSTANCE;
}
Expand All @@ -70,6 +76,13 @@ private class MicroProfileListener

private static final String JAVA_FILE_EXTENSION = "java";

// Lock for synchronizing access to pending event state
private final Object eventLock = new Object();
// Event waiting to be fired after debounce delay
private MicroProfilePropertiesChangeEvent pendingEvent = null;
// Scheduled task for firing the pending event
private ScheduledFuture<?> scheduledNotification = null;

@Override
public void elementChanged(ElementChangedEvent event) {
if (listeners.isEmpty()) {
Expand Down Expand Up @@ -188,22 +201,72 @@ public boolean visit(IResourceDelta delta) throws CoreException {
}

private void fireAsyncEvent(MicroProfilePropertiesChangeEvent event) {
// IMPORTANT: The LSP notification 'microprofile/propertiesChanged' must be
// executed
// in background otherwise it breaks everything (JDT LS for Java completion,
// hover, etc are broken)
CompletableFuture.runAsync(() -> {
for (IMicroProfilePropertiesChangedListener listener : listeners) {
try {
listener.propertiesChanged(event);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE,
"Error while sending LSP 'microprofile/propertiesChanged' notification", e);
}
synchronized (eventLock) {
// Merge with pending event if one exists
if (pendingEvent == null) {
pendingEvent = event;
} else {
mergeEvents(pendingEvent, event);
}

// Cancel previous timer if it exists
if (scheduledNotification != null && !scheduledNotification.isDone()) {
scheduledNotification.cancel(false);
}

// Schedule notification after debounce delay
scheduledNotification = scheduler.schedule(() -> {
MicroProfilePropertiesChangeEvent eventToFire;
synchronized (eventLock) {
eventToFire = pendingEvent;
pendingEvent = null;
scheduledNotification = null;
}

if (eventToFire != null) {
notifyListeners(eventToFire);
}
}, DEBOUNCE_DELAY_MS, TimeUnit.MILLISECONDS);
}
}

/**
* Merges two events by combining their project URIs and taking the widest
* scope. Scope hierarchy: SOURCES_AND_DEPENDENCIES > ONLY_SOURCES >
* ONLY_CONFIG_FILES
*/
private void mergeEvents(MicroProfilePropertiesChangeEvent target, MicroProfilePropertiesChangeEvent source) {
// Merge project URIs
if (source.getProjectURIs() != null) {
if (target.getProjectURIs() == null) {
target.setProjectURIs(new HashSet<>());
}
target.getProjectURIs().addAll(source.getProjectURIs());
}

// Handle event type - take the widest scope
if (source.getType() == MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES) {
target.setType(MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES);
} else if (source.getType() == MicroProfilePropertiesScope.ONLY_SOURCES
&& target.getType() != MicroProfilePropertiesScope.SOURCES_AND_DEPENDENCIES) {
target.setType(MicroProfilePropertiesScope.ONLY_SOURCES);
}
}

/**
* Notifies all registered listeners about the properties change event.
*/
private void notifyListeners(MicroProfilePropertiesChangeEvent event) {
for (IMicroProfilePropertiesChangedListener listener : listeners) {
try {
listener.propertiesChanged(event);
} catch (Exception e) {
if (LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE,
"Error while sending LSP 'microprofile/propertiesChanged' notification", e);
}
}
});
}
}

private boolean isJavaFile(IFile file) {
Expand All @@ -224,6 +287,8 @@ private boolean isFileContentChanged(IResourceDelta delta) {

private final Set<IMicroProfilePropertiesChangedListener> listeners;

private ScheduledExecutorService scheduler;

private MicroProfilePropertiesListenerManager() {
listeners = new HashSet<>();
}
Expand Down Expand Up @@ -257,6 +322,11 @@ public synchronized void initialize() {
if (microprofileListener != null) {
return;
}
this.scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "MicroProfile-Properties-Debouncer");
t.setDaemon(true);
return t;
});
this.microprofileListener = new MicroProfileListener();
JavaCore.addElementChangedListener(microprofileListener);
ResourcesPlugin.getWorkspace().addResourceChangeListener(microprofileListener,
Expand All @@ -272,6 +342,18 @@ public synchronized void destroy() {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(microprofileListener);
this.microprofileListener = null;
}
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
scheduler = null;
}
}

}
}
Loading