Skip to content

Commit 8a8b77f

Browse files
committed
Tell the user when the embedded native binary is outdated
Once the plugin moves to a new version, the binary downloaded for the previous one keeps formatting. When a project opens where it runs, open-java-format and the native formatter on and no binary of the user's own, a notification says so and offers Download and Ignore, as the IDE does about outdated spelling dictionaries. Ignore holds for that version only, kept in the application's PropertiesComponent. With no binary at all it offers the download, since the Java-based formatter is what runs meanwhile. Once per IDE session, however many projects open. The same activity removes what an earlier session left behind: an old version Windows held open while it formatted, or a download cut short.
1 parent 6c004d0 commit 8a8b77f

5 files changed

Lines changed: 228 additions & 0 deletions

File tree

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/EmbeddedNativeImage.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.Objects;
4141
import java.util.Optional;
4242
import java.util.concurrent.CopyOnWriteArrayList;
43+
import java.util.concurrent.atomic.AtomicBoolean;
4344
import java.util.concurrent.atomic.AtomicReference;
4445
import java.util.regex.Pattern;
4546
import javax.annotation.Nullable;
@@ -67,6 +68,7 @@ final class EmbeddedNativeImage {
6768

6869
private final AtomicReference<Activity> activity = new AtomicReference<>(new Activity.Idle());
6970
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
71+
private final AtomicBoolean noticeShown = new AtomicBoolean();
7072

7173
EmbeddedNativeImage() {
7274
this(
@@ -265,6 +267,11 @@ private void removeAllBut(String version) {
265267
}
266268
}
267269

270+
/** True the first time it is asked in an IDE session: the notice about the binary is shown once, not per project. */
271+
boolean claimSessionNotice() {
272+
return noticeShown.compareAndSet(false, true);
273+
}
274+
268275
/** Calls the listener on the EDT whenever the status may have changed, until parent is disposed. */
269276
void addChangeListener(Runnable listener, Disposable parent) {
270277
listeners.add(listener);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.palantir.javaformat.intellij;
16+
17+
import com.intellij.ide.util.PropertiesComponent;
18+
import com.intellij.notification.NotificationAction;
19+
import com.intellij.notification.NotificationGroupManager;
20+
import com.intellij.notification.NotificationType;
21+
import com.intellij.openapi.project.Project;
22+
import com.intellij.openapi.startup.StartupActivity;
23+
import com.palantir.javaformat.intellij.EmbeddedNativeImage.Status;
24+
import java.util.Optional;
25+
import javax.annotation.Nullable;
26+
import org.jetbrains.annotations.NotNull;
27+
28+
/**
29+
* Tells the user, as the IDE does about outdated spelling dictionaries, when the embedded native binary is older than
30+
* the plugin, or missing although it is switched on. Once per IDE session, however many projects open, and never
31+
* again for a version the user ignored.
32+
*/
33+
public final class NativeImageNoticeStartupActivity implements StartupActivity.DumbAware {
34+
35+
static final String IGNORED_VERSION = "open-java-format.native.ignoredVersion";
36+
37+
sealed interface Notice {
38+
record Outdated(String installed, String available) implements Notice {}
39+
40+
record Missing() implements Notice {}
41+
}
42+
43+
@Override
44+
public void runActivity(@NotNull Project project) {
45+
var embedded = EmbeddedNativeImage.getInstance();
46+
embedded.removeStale();
47+
48+
var settings = PalantirJavaFormatSettings.getInstance(project);
49+
var pluginVersion = settings.getImplementationVersion().orElse("unknown");
50+
var properties = PropertiesComponent.getInstance();
51+
var notice = notice(settings, embedded.status(pluginVersion), properties.getValue(IGNORED_VERSION));
52+
if (notice.isEmpty() || !embedded.claimSessionNotice()) {
53+
return;
54+
}
55+
56+
var group = NotificationGroupManager.getInstance()
57+
.getNotificationGroup(Notifications.NATIVE_IMAGE_NOTIFICATION_GROUP);
58+
var notification = switch (notice.get()) {
59+
case Notice.Outdated outdated ->
60+
group.createNotification(
61+
Notifications.nativeImageOutdatedMessage(outdated.installed(), outdated.available()),
62+
NotificationType.INFORMATION)
63+
.addAction(NotificationAction.createSimpleExpiring(
64+
"Download", () -> embedded.download(project, outdated.available())))
65+
.addAction(NotificationAction.createSimpleExpiring(
66+
"Ignore", () -> properties.setValue(IGNORED_VERSION, outdated.available())));
67+
case Notice.Missing missing ->
68+
group.createNotification(Notifications.nativeImageMissingMessage(), NotificationType.WARNING)
69+
.addAction(NotificationAction.createSimpleExpiring(
70+
"Download", () -> embedded.download(project, pluginVersion)));
71+
};
72+
notification.setTitle("open-java-format").notify(project);
73+
}
74+
75+
/**
76+
* What to tell the user about a project's embedded binary, if anything. Only where it runs: open-java-format on,
77+
* the native formatter on, and no binary of the user's own.
78+
*/
79+
static Optional<Notice> notice(
80+
PalantirJavaFormatSettings settings, Status status, @Nullable String ignoredVersion) {
81+
if (!settings.isEnabled()
82+
|| !settings.useNativeImage()
83+
|| settings.getNativeImagePath().isPresent()) {
84+
return Optional.empty();
85+
}
86+
if (status instanceof Status.NotDownloaded) {
87+
return Optional.of(new Notice.Missing());
88+
}
89+
if (status instanceof Status.Outdated outdated && !outdated.available().equals(ignoredVersion)) {
90+
return Optional.of(new Notice.Outdated(outdated.installed().version(), outdated.available()));
91+
}
92+
return Optional.empty();
93+
}
94+
}

‎open-java-format-idea-plugin/src/main/java/com/palantir/javaformat/intellij/Notifications.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Notifications {
2121
static final String GENERIC_ERROR_NOTIFICATION_GROUP = "open-java-format error";
2222
static final String PARSING_ERROR_NOTIFICATION_GROUP = "open-java-format parsing error";
2323
static final String PARSING_ERROR_TITLE = PARSING_ERROR_NOTIFICATION_GROUP;
24+
static final String NATIVE_IMAGE_NOTIFICATION_GROUP = "open-java-format native binary";
2425

2526
static String parsingErrorMessage(String filename) {
2627
return "open-java-format failed. Does " + filename + " have syntax errors?";
@@ -29,4 +30,12 @@ static String parsingErrorMessage(String filename) {
2930
static String formatterFailedMessage(String filename) {
3031
return "open-java-format could not run the formatter on " + filename + ". The details are in idea.log.";
3132
}
33+
34+
static String nativeImageOutdatedMessage(String installed, String available) {
35+
return "The native binary " + installed + " is outdated, " + available + " is available.";
36+
}
37+
38+
static String nativeImageMissingMessage() {
39+
return "The native binary is not downloaded, so the Java-based formatter formats instead.";
40+
}
3241
}

‎open-java-format-idea-plugin/src/main/resources/META-INF/plugin.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@
2727
nonDefaultProject="true"/>
2828
<projectService serviceImplementation="com.palantir.javaformat.intellij.PalantirJavaFormatSettings"/>
2929
<postStartupActivity implementation="com.palantir.javaformat.intellij.InitialConfigurationStartupActivity"/>
30+
<postStartupActivity implementation="com.palantir.javaformat.intellij.NativeImageNoticeStartupActivity"/>
3031
<notificationGroup displayType="BALLOON" id="open-java-format parsing error"
3132
isLogByDefault="false"/>
33+
<notificationGroup displayType="STICKY_BALLOON" id="open-java-format native binary"/>
3234
</extensions>
3335

3436
</idea-plugin>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
15+
package com.palantir.javaformat.intellij;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
19+
import com.intellij.notification.NotificationGroupManager;
20+
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor;
21+
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory;
22+
import com.intellij.testFramework.fixtures.JavaTestFixtureFactory;
23+
import com.palantir.javaformat.intellij.EmbeddedNativeImage.Installed;
24+
import com.palantir.javaformat.intellij.EmbeddedNativeImage.Status;
25+
import com.palantir.javaformat.intellij.NativeImageNoticeStartupActivity.Notice;
26+
import java.nio.file.Path;
27+
import java.util.Optional;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
31+
final class NativeImageNoticeStartupActivityTest {
32+
33+
private static final Installed INSTALLED =
34+
new Installed("21.0.1", Path.of("/system/open-java-format/native/21.0.1/open-java-format"));
35+
private static final Status OUTDATED = new Status.Outdated(INSTALLED, "21.0.2");
36+
37+
private final PalantirJavaFormatSettings settings = new PalantirJavaFormatSettings();
38+
39+
@BeforeEach
40+
void setUp() {
41+
settings.setEnabled(true);
42+
settings.setUseNativeImage(true);
43+
}
44+
45+
@Test
46+
void offersTheNewerBinary() {
47+
assertThat(NativeImageNoticeStartupActivity.notice(settings, OUTDATED, null))
48+
.hasValue(new Notice.Outdated("21.0.1", "21.0.2"));
49+
}
50+
51+
@Test
52+
void offersADownloadWhenThereIsNoBinary() {
53+
assertThat(NativeImageNoticeStartupActivity.notice(settings, new Status.NotDownloaded(), null))
54+
.hasValue(new Notice.Missing());
55+
}
56+
57+
@Test
58+
void staysQuietAboutAVersionTheUserIgnored() {
59+
assertThat(NativeImageNoticeStartupActivity.notice(settings, OUTDATED, "21.0.2"))
60+
.isEmpty();
61+
// Ignore holds for one version: the next one is offered again.
62+
assertThat(NativeImageNoticeStartupActivity.notice(settings, OUTDATED, "21.0.1"))
63+
.isPresent();
64+
}
65+
66+
@Test
67+
void staysQuietWhenTheEmbeddedBinaryDoesNotRun() {
68+
var custom = new PalantirJavaFormatSettings();
69+
custom.setEnabled(true);
70+
custom.setUseNativeImage(true);
71+
custom.setNativeImagePath(Optional.of(Path.of("/opt/open-java-format")));
72+
var javaBased = new PalantirJavaFormatSettings();
73+
javaBased.setEnabled(true);
74+
var disabled = new PalantirJavaFormatSettings();
75+
disabled.setUseNativeImage(true);
76+
77+
assertThat(NativeImageNoticeStartupActivity.notice(custom, OUTDATED, null))
78+
.isEmpty();
79+
assertThat(NativeImageNoticeStartupActivity.notice(javaBased, OUTDATED, null))
80+
.isEmpty();
81+
assertThat(NativeImageNoticeStartupActivity.notice(disabled, OUTDATED, null))
82+
.isEmpty();
83+
}
84+
85+
@Test
86+
void staysQuietAboutACurrentBinary() {
87+
assertThat(NativeImageNoticeStartupActivity.notice(settings, new Status.Current(INSTALLED), null))
88+
.isEmpty();
89+
assertThat(NativeImageNoticeStartupActivity.notice(settings, new Status.NoImageForPlatform(), null))
90+
.isEmpty();
91+
}
92+
93+
@Test
94+
void tellsTheSessionOnlyOnce() {
95+
var embedded = new EmbeddedNativeImage(Path.of("unused"), Optional.empty(), "http://127.0.0.1:1");
96+
97+
assertThat(embedded.claimSessionNotice()).isTrue();
98+
assertThat(embedded.claimSessionNotice()).isFalse();
99+
}
100+
101+
@Test
102+
void registersItsNotificationGroup() throws Exception {
103+
var builder = IdeaTestFixtureFactory.getFixtureFactory()
104+
.createLightFixtureBuilder(
105+
new DefaultLightProjectDescriptor(), getClass().getName());
106+
var fixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(builder.getFixture());
107+
fixture.setUp();
108+
try {
109+
assertThat(NotificationGroupManager.getInstance()
110+
.getNotificationGroup(Notifications.NATIVE_IMAGE_NOTIFICATION_GROUP))
111+
.isNotNull();
112+
} finally {
113+
fixture.tearDown();
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)