diff --git a/ide/extexecution.base/src/org/netbeans/api/extexecution/base/BaseExecutionService.java b/ide/extexecution.base/src/org/netbeans/api/extexecution/base/BaseExecutionService.java
index f84e9589add4..44d5431b8eb3 100644
--- a/ide/extexecution.base/src/org/netbeans/api/extexecution/base/BaseExecutionService.java
+++ b/ide/extexecution.base/src/org/netbeans/api/extexecution/base/BaseExecutionService.java
@@ -140,6 +140,45 @@ public static BaseExecutionService newService(@NonNull Callable extends Proces
return new BaseExecutionService(processCreator, descriptor);
}
+ /**
+ * Infers the output encoding from the relevant system properties, if those should all be null
+ * then this will fallback to Charset.defaultCharset()
+ *
+ * Since JDK 18 and JEP400 Console.charset() is used for the console's encoding instead of Charset.defaultCharset().
+ * Console.charset() is exposed via stdout.encoding/sun.stdout.encoding.
+ * If ran with JDK<=16 stdout.encoding and native.encoding should be null and the old default of Charset.defaultCharset() will be used to match pre JEP400 behavior.
+ *
+ * The checking order for the encoding is stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset()
+ *
+ * @see org.netbeans.modules.maven.execute.CommandLineOutputHandler#getPreferredCharset
+ *
+ * @return inferred encoding as Charset
+ */
+ private static Charset getInputOutputEncoding(){
+ String[] encodingSystemProperties = new String[]{"stdout.encoding", "sun.stdout.encoding", "native.encoding"};
+
+ Charset preferredCharset = null;
+ for (String encodingProperty : encodingSystemProperties) {
+ String encodingPropertyValue = System.getProperty(encodingProperty);
+ if (encodingPropertyValue == null) {
+ continue;
+ }
+
+ try {
+ preferredCharset = Charset.forName(encodingPropertyValue);
+ } catch (IllegalArgumentException ex) {
+ LOGGER.log(java.util.logging.Level.WARNING, "Failed to get charset for '" + encodingProperty + "' value : '" + encodingPropertyValue + "'", ex);
+ }
+
+ if (preferredCharset != null) {
+ return preferredCharset;
+ }
+
+ }
+
+ return Charset.defaultCharset();
+ }
+
/**
* Runs the process described by this service. The call does not block
* and the task is represented by the returned value. Integer returned
@@ -209,28 +248,17 @@ public Integer call() throws Exception {
executor = Executors.newFixedThreadPool(in != null ? 3 : 2);
Charset charset = descriptor.getCharset();
+ // The CommandLineOutputHandler used the default charset to convert
+ // output from command line invocations to strings. That encoding is
+ // derived from the system file.encoding. From JDK 18 onwards its
+ // default value changed to UTF-8.
+ // JDK 17+ exposes the native encoding as the new system property
+ // native.encoding, prior versions don't have that property and will
+ // report NULL for it.
+ // To account for the behavior of JEP400 the following order is used to determine the encoding:
+ // stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset()
if (charset == null) {
- // If charset is not set for the descriptor, the
- // BaseExecutionService used the default charset to convert
- // output from command line invocations to strings. That encoding is
- // derived from the system file.encoding. From JDK 18 onwards its
- // default value changed to UTF-8.
- // JDK 18+ exposes the native encoding as the new system property
- // native.encoding, prior versions don't have that property and will
- // report NULL for it.
- // The algorithm is simple: If native.encoding is set, it will be used
- // else the old default will be queried via Charset#defaultCharset.
- String nativeEncoding = System.getProperty("native.encoding");
- if (nativeEncoding != null) {
- try {
- charset = Charset.forName(nativeEncoding);
- } catch (Exception ex) {
- LOGGER.log(java.util.logging.Level.WARNING, "Failed to get charset for native.encoding value : '" + nativeEncoding + "'", ex);
- }
- }
- if (charset == null) {
- charset = Charset.defaultCharset();
- }
+ charset = BaseExecutionService.getInputOutputEncoding();
}
tasks.add(InputReaderTask.newDrainingTask(
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/parsing/PatchModuleFileManager.java b/java/java.source.base/src/org/netbeans/modules/java/source/parsing/PatchModuleFileManager.java
index aed1075b0d05..ed9f6f920050 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/parsing/PatchModuleFileManager.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/parsing/PatchModuleFileManager.java
@@ -21,6 +21,7 @@
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
@@ -29,6 +30,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -43,6 +45,7 @@
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.java.classpath.ClassPath;
+import org.netbeans.api.java.queries.BinaryForSourceQuery;
import org.netbeans.modules.java.source.indexing.JavaIndex;
import org.netbeans.modules.java.source.util.Iterators;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
@@ -64,6 +67,7 @@ final class PatchModuleFileManager implements JavaFileManager {
private final Map> patches;
private final Map roots;
private Set moduleLocations;
+ private String overrideModuleName;
PatchModuleFileManager(
@NonNull final JavaFileManager binDelegate,
@@ -162,7 +166,8 @@ public boolean handleOption(String head, Iterator tail) {
return true;
}
} else if (head.startsWith(JavacParser.NB_X_MODULE)) {
- addModulePatches(head.substring(JavacParser.NB_X_MODULE.length()),
+ overrideModuleName = head.substring(JavacParser.NB_X_MODULE.length());
+ addModulePatches(overrideModuleName,
src.entries().stream().map(e -> e.getURL()).collect(Collectors.toList()));
return true;
}
@@ -214,6 +219,8 @@ public ClassLoader getClassLoader(Location location) {
@Override
public Iterable list(Location location, String packageName, Set kinds, boolean recurse) throws IOException {
+ location = fixLocation(location);
+
if (PatchLocation.isInstance(location)) {
final PatchLocation pl = PatchLocation.cast(location);
final ModuleLocation bin = pl.getBin();
@@ -249,6 +256,8 @@ public String inferBinaryName(Location location, JavaFileObject file) {
@Override
public JavaFileObject getJavaFileForInput(Location location, String className, JavaFileObject.Kind kind) throws IOException {
+ location = fixLocation(location);
+
if (PatchLocation.isInstance(location)) {
final PatchLocation pl = PatchLocation.cast(location);
final ModuleLocation bin = pl.getBin();
@@ -285,6 +294,14 @@ public FileObject getFileForOutput(Location location, String packageName, String
}
//
+ private Location fixLocation(Location input) throws IOException {
+ if (input == StandardLocation.CLASS_OUTPUT && overrideModuleName != null) {
+ return this.moduleLocations.stream().filter(pl -> overrideModuleName.equals(pl.getModuleName())).map(pl -> (Location) pl).findAny().orElse(input);
+ } else {
+ return input;
+ }
+ }
+
private Set moduleLocations(final Location baseLocation) throws IOException {
if (baseLocation != StandardLocation.PATCH_MODULE_PATH) {
throw new IllegalStateException(baseLocation.toString());
@@ -292,7 +309,7 @@ private Set moduleLocations(final Location baseLocation) throws I
if (moduleLocations == null) {
Set res = new HashSet<>();
for (Map.Entry> patch : patches.entrySet()) {
- res.add(createPatchLocation(patch.getKey(), patch.getValue()));
+ res.add(createPatchLocation(patch.getKey(), patch.getValue(), patch.getKey().equals(overrideModuleName)));
}
moduleLocations = Collections.unmodifiableSet(res);
}
@@ -302,7 +319,8 @@ private Set moduleLocations(final Location baseLocation) throws I
@NonNull
private static PatchLocation createPatchLocation(
@NonNull final String modName,
- @NonNull final List extends URL> roots) throws IOException {
+ @NonNull final List extends URL> roots,
+ final boolean sourceOverride) throws IOException {
Collection bin = new ArrayList<>(roots.size());
Collection src = new ArrayList<>(roots.size());
for (URL root : roots) {
@@ -310,7 +328,11 @@ private static PatchLocation createPatchLocation(
src.add(root);
bin.add(FileUtil.urlForArchiveOrDir(JavaIndex.getClassFolder(root)));
} else {
- bin.add(root);
+ if (sourceOverride) {
+ bin.addAll(Arrays.asList(BinaryForSourceQuery.findBinaryRoots(root).getRoots()));
+ } else {
+ bin.add(root);
+ }
}
}
return new PatchLocation(
diff --git a/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/parsing/PatchModuleTest.java b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/parsing/PatchModuleTest.java
new file mode 100644
index 000000000000..0dfcf604c106
--- /dev/null
+++ b/java/java.source.base/test/unit/src/org/netbeans/modules/java/source/parsing/PatchModuleTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.java.source.parsing;
+
+import com.sun.tools.javac.api.JavacTool;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import javax.swing.event.ChangeListener;
+import javax.tools.ForwardingJavaFileManager;
+import javax.tools.JavaFileManager;
+import javax.tools.JavaFileManager.Location;
+import javax.tools.JavaFileObject;
+import javax.tools.JavaFileObject.Kind;
+import javax.tools.SimpleJavaFileObject;
+import static junit.framework.TestCase.assertEquals;
+import org.netbeans.api.java.queries.BinaryForSourceQuery;
+import org.netbeans.api.java.source.ClasspathInfo;
+import org.netbeans.api.java.source.CompilationController;
+import org.netbeans.api.java.source.JavaSource;
+import org.netbeans.api.java.source.JavaSource.Phase;
+import org.netbeans.api.java.source.SourceUtilsTestUtil;
+import org.netbeans.api.java.source.Task;
+import org.netbeans.api.java.source.TestUtilities;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.java.source.tasklist.CompilerSettings;
+import org.netbeans.modules.java.source.usages.ClasspathInfoAccessor;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.netbeans.spi.java.queries.BinaryForSourceQueryImplementation;
+
+public class PatchModuleTest extends NbTestCase {
+
+ public PatchModuleTest(String name) {
+ super(name);
+ }
+
+ private FileObject sourceRoot;
+ private FileObject classRoot;
+ private FileObject cp;
+
+ protected void setUp() throws Exception {
+ SourceUtilsTestUtil.prepareTest(new String[0], new Object[] {settings, binaryForSource});
+ clearWorkDir();
+ prepareTest();
+ }
+
+ public void testNETBEANS_4044() throws Exception {
+ settings.commandLine = "-Xnb-Xmodule:patch";
+ binaryForSource.result = new BinaryForSourceQuery.Result() {
+ @Override
+ public URL[] getRoots() {
+ return new URL[] {classRoot.toURL()};
+ }
+ @Override
+ public void addChangeListener(ChangeListener l) {}
+ @Override
+ public void removeChangeListener(ChangeListener l) {}
+ };
+ FileObject sourceModule = createFile("module-info.java", "module patch {}"); SourceUtilsTestUtil.setSourceLevel(sourceModule, "11");
+ FileObject source1 = createFile("patch/Patch.java", "package patch; class Patch { Dep dep; }"); SourceUtilsTestUtil.setSourceLevel(source1, "11");
+ FileObject source2 = createFile("patch/Dep.java", "package patch; class Dep { }"); SourceUtilsTestUtil.setSourceLevel(source2, "11");
+ ClasspathInfo cpInfo = ClasspathInfo.create(sourceModule);
+ try (JavaFileManager fm = ClasspathInfoAccessor.getINSTANCE().createFileManager(cpInfo, "11");
+ JavaFileManager output = new OutputFileManager(fm)) {
+ List files = new ArrayList<>();
+ Enumeration extends FileObject> en = sourceRoot.getChildren(true);
+ while (en.hasMoreElements()) {
+ FileObject f = en.nextElement();
+ if (f.isData()) {
+ files.add(FileObjects.fileObjectFileObject(f, sourceRoot, null, null));
+ }
+ }
+ assertTrue(JavacTool.create().getTask(null, output, null, null, null, files).call());
+ }
+ JavaSource js = JavaSource.forFileObject(source1);
+
+ js.runUserActionTask(new Task() {
+ public void run(CompilationController parameter) throws Exception {
+ assertTrue(Phase.RESOLVED.compareTo(parameter.toPhase(Phase.RESOLVED)) <= 0);
+ assertEquals(parameter.getDiagnostics().toString(), 0, parameter.getDiagnostics().size());
+ }
+ }, true);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ settings.commandLine = null;
+ binaryForSource.result = null;
+ }
+
+ private FileObject createFile(String path, String content) throws Exception {
+ FileObject file = FileUtil.createData(sourceRoot, path);
+ TestUtilities.copyStringToFile(file, content);
+
+ return file;
+ }
+
+ private void prepareTest() throws Exception {
+ File work = getWorkDir();
+ FileObject workFO = FileUtil.toFileObject(work);
+
+ assertNotNull(workFO);
+
+ sourceRoot = workFO.createFolder("src");
+ classRoot = workFO.createFolder("class");
+
+ FileObject buildRoot = workFO.createFolder("build");
+ FileObject cache = workFO.createFolder("cache");
+
+ SourceUtilsTestUtil.prepareTest(sourceRoot, buildRoot, cache, new FileObject[] {cp});
+ }
+
+ private static final CompilerSettingsImpl settings = new CompilerSettingsImpl();
+
+ private static final class CompilerSettingsImpl extends CompilerSettings {
+ private String commandLine;
+ @Override
+ protected String buildCommandLine(FileObject file) {
+ return commandLine;
+ }
+
+ }
+
+ private static final BinaryForSourceQueryImpl binaryForSource = new BinaryForSourceQueryImpl();
+
+ private static final class BinaryForSourceQueryImpl implements BinaryForSourceQueryImplementation {
+
+ private BinaryForSourceQuery.Result result;
+
+ @Override
+ public BinaryForSourceQuery.Result findBinaryRoots(URL sourceRoot) {
+ return result;
+ }
+
+ }
+
+ private class OutputFileManager extends ForwardingJavaFileManager {
+
+ public OutputFileManager(JavaFileManager fileManager) {
+ super(fileManager);
+ }
+
+ @Override
+ public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, javax.tools.FileObject sibling) throws IOException {
+ try {
+ return new SimpleJavaFileObject(new URI("mem://" + className + kind.extension), kind) {
+ @Override
+ public OutputStream openOutputStream() throws IOException {
+ return FileUtil.createData(classRoot, className.replace(".", "/") + kind.extension).getOutputStream();
+ }
+ };
+ } catch (URISyntaxException ex) {
+ throw new IOException(ex);
+ }
+ }
+ }
+}
diff --git a/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java b/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java
index 3b73f5f1f84b..8ad34b0a29ff 100644
--- a/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java
+++ b/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java
@@ -202,7 +202,7 @@ private class Output implements Runnable {
private boolean skipLF = false;
public Output(InputStream instream) {
- str = new BufferedReader(new InputStreamReader(instream, getNativeCharset()));
+ str = new BufferedReader(new InputStreamReader(instream, getPreferredCharset()));
}
private String readLine() throws IOException {
@@ -701,7 +701,7 @@ public void stopInput() {
public @Override void run() {
Reader in = inputOutput.getIn();
- try (Writer out = new OutputStreamWriter(str, getNativeCharset())) {
+ try (Writer out = new OutputStreamWriter(str, getPreferredCharset())) {
while (true) {
int read = in.read();
if (read != -1) {
@@ -827,30 +827,45 @@ public ExecutionEventObject.Tree getExecutionTree() {
}
}
-
- private static Charset getNativeCharset() {
+
+ /**
+ * Returns the preferred Charset that is obtained by checking the following system properties:
+ * stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset()
+ * @see org.netbeans.api.extexecution.base.BaseExecutionService#getInputOutputEncoding
+ * @return Charset
+ */
+ private static Charset getPreferredCharset() {
// The CommandLineOutputHandler used the default charset to convert
// output from command line invocations to strings. That encoding is
// derived from the system file.encoding. From JDK 18 onwards its
// default value changed to UTF-8.
- // JDK 18+ exposes the native encoding as the new system property
+ // JDK 17+ exposes the native encoding as the new system property
// native.encoding, prior versions don't have that property and will
// report NULL for it.
- // The algorithm is simple: If native.encoding is set, it will be used
- // else the old default will be queried via Charset#defaultCharset.
- String nativeEncoding = System.getProperty("native.encoding");
- Charset nativeCharset = null;
- if (nativeEncoding != null) {
+ // To account for the behavior of JEP400 the following order is used to determine the encoding:
+ // stdout.encoding, sun.stdout.encoding, native.encoding, Charset.defaultCharset()
+ String[] encodingSystemProperties = new String[]{"stdout.encoding", "sun.stdout.encoding", "native.encoding"};
+
+ Charset preferredCharset = null;
+ for (String encodingProperty : encodingSystemProperties) {
+ String encodingPropertyValue = System.getProperty(encodingProperty);
+ if (encodingPropertyValue == null) {
+ continue;
+ }
+
try {
- nativeCharset = Charset.forName(nativeEncoding);
- } catch (Exception ex) {
- LOG.log(java.util.logging.Level.WARNING, "Failed to get charset for native.encoding value : '" + nativeEncoding + "'", ex);
+ preferredCharset = Charset.forName(encodingPropertyValue);
+ } catch (IllegalArgumentException ex) {
+ LOG.log(java.util.logging.Level.WARNING, "Failed to get charset for '" + encodingProperty + "' value : '" + encodingPropertyValue + "'", ex);
}
+
+ if (preferredCharset != null) {
+ return preferredCharset;
+ }
+
}
- if (nativeCharset == null) {
- nativeCharset = Charset.defaultCharset();
- }
- return nativeCharset;
+
+ return Charset.defaultCharset();
}
}