From 25101b48e9c207494db83065a17c74119d3e7a4b Mon Sep 17 00:00:00 2001 From: Nicola Isotta Date: Thu, 22 Jan 2026 19:00:52 +0100 Subject: [PATCH] Improve goto declaration for ResourceBundles in JSF files and improve rendering The goto declaration implementation now does not directly jump to the base properties file, but give the use the option to choose the intent property file also offering the other language files. Rendering of the popup was updated and svg icons added. --- .../web.el/nbproject/project.properties | 2 +- .../modules/web/el/ELOccurrencesFinder.java | 18 +- .../modules/web/el/ResourceBundles.java | 300 ++++++++++++++---- .../completion/ELCodeCompletionHandler.java | 15 +- .../web/el/completion/ELCompletionUtil.java | 31 ++ .../el/completion/resources/propertiesKey.svg | 51 +++ .../completion/resources/propertiesLocale.svg | 54 ++++ .../web/el/hints/ResourceBundleKeys.java | 2 +- .../el/navigation/ELDeclarationFinder.java | 90 ++++-- .../el/navigation/ELHyperlinkProvider.java | 12 +- .../modules/web/el/spi/ResourceBundle.java | 13 +- .../jsf/editor/resources/propertiesKey.svg | 51 +++ .../jsf/editor/resources/propertiesLocale.svg | 54 ++++ .../editor/JSFResourceBundlesProvider.java | 77 +++-- .../modules/properties/propertiesKey.svg | 51 +++ .../modules/properties/propertiesLocale.svg | 54 ++++ 16 files changed, 725 insertions(+), 150 deletions(-) create mode 100644 enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCompletionUtil.java create mode 100644 enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesKey.svg create mode 100644 enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesLocale.svg create mode 100644 enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesKey.svg create mode 100644 enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesLocale.svg create mode 100644 ide/properties/src/org/netbeans/modules/properties/propertiesKey.svg create mode 100644 ide/properties/src/org/netbeans/modules/properties/propertiesLocale.svg diff --git a/enterprise/web.el/nbproject/project.properties b/enterprise/web.el/nbproject/project.properties index f753aa35afd5..c81e65023d7c 100644 --- a/enterprise/web.el/nbproject/project.properties +++ b/enterprise/web.el/nbproject/project.properties @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -javac.source=1.8 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial test-unit-sys-prop.web.project.jars=\ diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/ELOccurrencesFinder.java b/enterprise/web.el/src/org/netbeans/modules/web/el/ELOccurrencesFinder.java index 1134afae8f0c..ea0c1fc892ea 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/ELOccurrencesFinder.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/ELOccurrencesFinder.java @@ -133,19 +133,19 @@ private void computeOccurrences(final ELParserResult parserResult) { try { jsource.runUserActionTask((CompilationController info) -> { info.toPhase(JavaSource.Phase.RESOLVED); - occurrences.putAll(findMatchingTypes(CompilationContext.create(file, info), target, matching)); + CompilationContext ccontext = CompilationContext.create(file, info); + occurrences.putAll(findMatchingTypes(ccontext, target, matching)); + if (this.occurrences.isEmpty()) { + // perhaps the caret is on a resource bundle key node + occurrences.putAll(findMatchingResourceBundleKeys(ccontext, target, parserResult)); + } }, true); } catch (IOException ex) { Exceptions.printStackTrace(ex); } - - if (this.occurrences.isEmpty()) { - // perhaps the caret is on a resource bundle key node - occurrences.putAll(findMatchingResourceBundleKeys(target, parserResult)); - } } - private Map findMatchingResourceBundleKeys(Pair target, ELParserResult parserResult) { + private Map findMatchingResourceBundleKeys(CompilationContext info, Pair target, ELParserResult parserResult) { ResourceBundles resourceBundles = ResourceBundles.get(parserResult.getFileObject()); if (!resourceBundles.canHaveBundles()) { return Collections.emptyMap(); @@ -154,7 +154,7 @@ private Map findMatchingResourceBundleKeys(Pair // the logic here is a bit strange, maybe should add new methods to ResourceBundles // for a more straightforward computation. // first, check whether the current EL elements has keys - keys.addAll(resourceBundles.collectKeys(target.first().getNode())); + keys.addAll(resourceBundles.collectKeys(target.first().getNode(), info.context())); if (keys.isEmpty()) { return Collections.emptyMap(); } @@ -176,7 +176,7 @@ private Map findMatchingResourceBundleKeys(Pair if (!each.isValid()) { continue; } - for (Pair candidate : resourceBundles.collectKeys(each.getNode())) { + for (Pair candidate : resourceBundles.collectKeys(each.getNode(), info.context())) { if (candidate.second().equals(target.second())) { OffsetRange range = each.getOriginalOffset(candidate.second()); result.put(range, ColoringAttributes.MARK_OCCURRENCES); diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/ResourceBundles.java b/enterprise/web.el/src/org/netbeans/modules/web/el/ResourceBundles.java index 5bf24f336df5..39706c19bee0 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/ResourceBundles.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/ResourceBundles.java @@ -24,8 +24,8 @@ import com.sun.el.parser.AstString; import com.sun.el.parser.Node; import java.io.IOException; -import java.net.URL; import java.util.*; +import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.text.BadLocationException; import javax.swing.text.StyledDocument; @@ -72,7 +72,7 @@ public final class ResourceBundles { private final WebModule webModule; private final Project project; - + /* bundle base name to ResourceBundleInfo map */ private Map bundlesMap; private long currentBundlesHashCode; @@ -82,12 +82,12 @@ public final class ResourceBundles { @Override public void fileChanged(FileEvent fe) { super.fileChanged(fe); - LOGGER.finer(String.format("File %s has changed.", fe.getFile())); //NOI18N + LOGGER.log(Level.FINER, "File {0} has changed.", fe.getFile()); //NOI18N resetResourceBundleMap(); } }; - + private ResourceBundles(WebModule webModule, Project project) { this.webModule = webModule; this.project = project; @@ -143,7 +143,19 @@ public boolean isResourceBundleIdentifier(String identifier, ResolverContext con * {@code key}; {@code false} otherwise. */ public boolean isValidKey(String bundle, String key) { - ResourceBundleInfo rbInfo = getBundleForIdentifier(bundle); + return isValidKey(new ResolverContext(), bundle, key); + } + + /** + * Checks whether the given {@code key} is defined in the given {@code bundle}. + * @param context non-null {@link ResolverContext} instance + * @param bundle the base name of the bundle. + * @param key the key to check. + * @return {@code true} if the given {@code bundle} exists and contains the given + * {@code key}; {@code false} otherwise. + */ + public boolean isValidKey(ResolverContext context, String bundle, String key) { + ResourceBundleInfo rbInfo = getBundleForIdentifier(context, bundle); if (rbInfo == null) { // no matching bundle file return true; @@ -158,12 +170,13 @@ public boolean isValidKey(String bundle, String key) { /** * Gets bundle info for given identifier. + * @param context non-null {@link ResolverContext} instance * @param ident identifier to examine * @return resource bundle info if any found, {@code null} otherwise */ - private ResourceBundleInfo getBundleForIdentifier(String ident) { + private ResourceBundleInfo getBundleForIdentifier(ResolverContext context, String ident) { // XXX - do it more efficiently - for (ResourceBundleInfo rbi : getBundlesMap().values()) { + for (ResourceBundleInfo rbi : getBundlesMap(context).values()) { if (ident.equals(rbi.getVarName())) { return rbi; } @@ -177,16 +190,21 @@ private ResourceBundleInfo getBundleForIdentifier(String ident) { * @return locations corresponding to given bundle name, never {@code null} */ public List getLocationsForBundleIdent(String ident) { - ResourceBundleInfo rbi = getBundleForIdentifier(ident); + return getLocationsForBundleIdent(new ResolverContext(), ident); + } + + /** + * Gets all locations for given bundle identifier. + * @param context non-null {@link ResolverContext} instance + * @param ident identifier of the bundle + * @return locations corresponding to given bundle name, never {@code null} + */ + public List getLocationsForBundleIdent(ResolverContext context, String ident) { + ResourceBundleInfo rbi = getBundleForIdentifier(context, ident); if (rbi == null) { return Collections.emptyList(); } - - List locations = new ArrayList<>(rbi.getFiles().size()); - for (FileObject fileObject : rbi.getFiles()) { - locations.add(new Location(0, fileObject)); - } - return locations; + return rbi.getFiles().stream().map(Location::new).toList(); } /** @@ -196,8 +214,19 @@ public List getLocationsForBundleIdent(String ident) { * @return locations (including the offset) of the searched key, never {@code null} */ public List getLocationsForBundleKey(String ident, String key) { + return getLocationsForBundleKey(new ResolverContext(), ident, key); + } + + /** + * Gets all locations for given bundle identifier and key. + * @param context non-null {@link ResolverContext} instance + * @param ident identifier of the bundle + * @param key key to search + * @return locations (including the offset) of the searched key, never {@code null} + */ + public List getLocationsForBundleKey(ResolverContext context, String ident, String key) { List locations = new ArrayList<>(); - for (Location location : getLocationsForBundleIdent(ident)) { + for (Location location : getLocationsForBundleIdent(context, ident)) { try { DataObject dobj = DataObject.find(location.getFile()); EditorCookie ec = dobj.getLookup().lookup(EditorCookie.class); @@ -210,11 +239,18 @@ public List getLocationsForBundleKey(String ident, String key) { if (lc != null) { Line.Set ls = lc.getLineSet(); for (Line line : ls.getLines()) { - if (line.getText().contains(key + "=") || line.getText().contains(key + " =")) { //NOI18N + String text = line.getText(); + if (text == null) { + continue; + } + text = text.trim(); + if (text.startsWith(key + "=") || text.startsWith(key + " =") + || text.startsWith(key + ":") || text.startsWith(key + " :")) { try { StyledDocument document = ec.getDocument(); int offset = document.getText(0, document.getLength()).indexOf(line.getText()); - locations.add(new Location(offset, location.getFile())); + locations.add(new Location(offset, location.getFile(), line.getLineNumber())); + break; } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } @@ -234,6 +270,7 @@ public List> collectKeys(final Node root) { /** * Collects references to resource bundle keys in the given {@code root}. + * @param context non-null {@link ResolverContext} instance * @return List of identifier/string pairs. Identifier = resource bundle base name - string = res bundle key. */ public List> collectKeys(final Node root, ResolverContext context) { @@ -261,6 +298,10 @@ public List> collectKeys(final Node root, ResolverCont } public String findResourceBundleIdentifier(AstPath astPath) { + return findResourceBundleIdentifier(new ResolverContext(), astPath); + } + + public String findResourceBundleIdentifier(ResolverContext context, AstPath astPath) { List path = astPath.leafToRoot(); for (int i = 0; i < path.size(); i++) { Node node = path.get(i); @@ -273,7 +314,7 @@ public String findResourceBundleIdentifier(AstPath astPath) { Node identifier = path.get(i + 2); if (brackets instanceof AstBracketSuffix && identifier instanceof AstIdentifier - && isResourceBundleIdentifier(identifier.getImage(), new ResolverContext())) { + && isResourceBundleIdentifier(identifier.getImage(), context)) { return identifier.getImage(); } } @@ -281,6 +322,7 @@ && isResourceBundleIdentifier(identifier.getImage(), new ResolverContext())) { } return null; } + /** * Gets the value of the given {@code key} in the given {@code bundle}. * @param bundle the base name of the bundle. @@ -288,7 +330,48 @@ && isResourceBundleIdentifier(identifier.getImage(), new ResolverContext())) { * @return the value or {@code null}. */ public String getValue(String bundle, String key) { - ResourceBundleInfo rbInfo = getBundlesMap().get(bundle); + return getValue(new ResolverContext(), bundle, key); + } + + /** + * Gets the value of the given {@code key} in the given {@code bundle}. + * @param context non-null {@link ResolverContext} instance + * @param bundle the base name of the bundle. + * @param key key in the given bundle. + * @return the value or {@code null}. + */ + public String getValue(ResolverContext context, String bundle, String key) { + ResourceBundleInfo rbInfo = getBundlesMap(context).get(bundle); + if (rbInfo == null || !rbInfo.getResourceBundle().containsKey(key)) { + // no matching bundle file + return null; + } + try { + return rbInfo.getResourceBundle().getString(key); + } catch (MissingResourceException e) { + return null; + } + } + + /** + * Gets the value of the given {@code key} in the given {@code bundle}. + * @param varName the var name of the bundle. + * @param key key in the given bundle. + * @return the value or {@code null}. + */ + public String getValueWithVarName(String varName, String key) { + return getValueWithVarName(new ResolverContext(), varName, key); + } + + /** + * Gets the value of the given {@code key} in the given {@code bundle}. + * @param context non-null {@link ResolverContext} instance + * @param varName the var name of the bundle. + * @param key key in the given bundle. + * @return the value or {@code null}. + */ + public String getValueWithVarName(ResolverContext context, String varName, String key) { + ResourceBundleInfo rbInfo = getBundleForIdentifier(context, varName); if (rbInfo == null || !rbInfo.getResourceBundle().containsKey(key)) { // no matching bundle file return null; @@ -306,8 +389,18 @@ public String getValue(String bundle, String key) { * @return */ public Map getEntries(String bundleVar) { - ResourceBundle bundle = findResourceBundleForVar(bundleVar); - ResourceBundleInfo rbInfo = getBundlesMap().get(bundle.getBaseName()); + return getEntries(new ResolverContext(), bundleVar); + } + + /** + * Gets the entries in the bundle identified by {@code bundleName}. + * @param context non-null {@link ResolverContext} instance + * @param bundleVar + * @return + */ + public Map getEntries(ResolverContext context, String bundleVar) { + ResourceBundle bundle = findResourceBundleForVar(context, bundleVar); + ResourceBundleInfo rbInfo = getBundlesMap(context).get(bundle.getBaseName()); if (rbInfo == null) { return Collections.emptyMap(); } @@ -318,11 +411,10 @@ public Map getEntries(String bundleVar) { } return result; } - - - private ResourceBundle findResourceBundleForVar(String variableName) { + + private ResourceBundle findResourceBundleForVar(ResolverContext context, String variableName) { List foundBundles = webModule != null ? - ELPlugin.Query.getResourceBundles(webModule.getDocumentBase(), new ResolverContext()) + ELPlugin.Query.getResourceBundles(webModule.getDocumentBase(), context) : Collections.emptyList(); //make the bundle var to bundle @@ -333,9 +425,11 @@ private ResourceBundle findResourceBundleForVar(String variableName) { } return null; } + /** * Finds list of all ResourceBundles, which are registered in all * JSF configuration files in a web module. + * @param context non-null {@link ResolverContext} instance */ public List getBundles(ResolverContext context) { FileObject docBase = webModule != null ? webModule.getDocumentBase() : null; @@ -345,17 +439,17 @@ public List getBundles(ResolverContext context) { /* * returns a map of bundle fully qualified name to java.util.ResourceBundle */ - private synchronized Map getBundlesMap() { - long bundlesHash = getBundlesHashCode(); + private synchronized Map getBundlesMap(ResolverContext context) { + long bundlesHash = getBundlesHashCode(context); if (bundlesMap == null) { currentBundlesHashCode = bundlesHash; - bundlesMap = createResourceBundleMapAndFileChangeListeners(); + bundlesMap = createResourceBundleMapAndFileChangeListeners(context); LOGGER.fine("New resource bundle map created."); //NOI18N } else { if(bundlesHash != currentBundlesHashCode) { //refresh the resource bundle map resetResourceBundleMap(); - bundlesMap = createResourceBundleMapAndFileChangeListeners(); + bundlesMap = createResourceBundleMapAndFileChangeListeners(context); currentBundlesHashCode = bundlesHash; LOGGER.fine("Resource bundle map recreated based on configuration changes."); //NOI18N @@ -364,7 +458,7 @@ private synchronized Map getBundlesMap() { return bundlesMap; } - + private synchronized void resetResourceBundleMap() { if(bundlesMap == null) { return ; @@ -372,25 +466,25 @@ private synchronized void resetResourceBundleMap() { for(ResourceBundleInfo info : bundlesMap.values()) { for (FileObject fileObject : info.getFiles()) { fileObject.removeFileChangeListener(FILE_CHANGE_LISTENER); - LOGGER.finer(String.format("Removed FileChangeListener from file %s", fileObject)); //NOI18N + LOGGER.log(Level.FINER, "Removed FileChangeListener from file {0}", fileObject); //NOI18N } } bundlesMap = null; LOGGER.fine("Resource bundle map released."); //NOI18N } - - private long getBundlesHashCode() { + + private long getBundlesHashCode(ResolverContext context) { //compute hashcode so we can compare if there are changes since the last time and possibly //reset the bundle map cache long hash = 3; - for(ResourceBundle rb : getBundles(new ResolverContext())) { + for(ResourceBundle rb : getBundles(context)) { hash = 11 * hash + rb.getBaseName().hashCode(); hash = 11 * hash + (rb.getVar() != null ? rb.getVar().hashCode() : 0); } return hash; } - - private Map createResourceBundleMapAndFileChangeListeners() { + + private Map createResourceBundleMapAndFileChangeListeners(ResolverContext context) { Map result = new HashMap<>(); ClassPathProvider provider = project.getLookup().lookup(ClassPathProvider.class); if (provider == null) { @@ -403,8 +497,26 @@ private Map createResourceBundleMapAndFileChangeList } SourceGroup[] sourceGroups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); - for (ResourceBundle bundle : getBundles(new ResolverContext())) { + for (ResourceBundle bundle : getBundles(context)) { String bundleFile = bundle.getBaseName(); + + for (FileObject fileObject : bundle.getFiles()) { + if (fileObject.canWrite()) { + fileObject.addFileChangeListener( + WeakListeners.create(FileChangeListener.class, FILE_CHANGE_LISTENER, fileObject)); + LOGGER.log(Level.FINER, "Added FileChangeListener to file {0}", fileObject); + } + } + + java.util.ResourceBundle found = null; + if (!bundle.getFiles().isEmpty()) { + found = loadBundleChain(bundle.getFiles()); + } + if (found != null) { + result.put(bundleFile, new ResourceBundleInfo(bundle.getFiles(), found, bundle.getVar())); + continue; + } + for (SourceGroup sourceGroup : sourceGroups) { FileObject rootFolder = sourceGroup.getRootFolder(); @@ -415,40 +527,94 @@ private Map createResourceBundleMapAndFileChangeList } ClassLoader classLoader = classPath.getClassLoader(false); try { - // TODO - rewrite listening on all (localized) files - String resourceFileName = new StringBuilder() - .append(bundleFile.replace(".", "/")) - .append(".properties") - .toString(); //NOI18N - - URL url = classLoader.getResource(resourceFileName); - if(url != null) { - LOGGER.finer(String.format("Found %s URL for resource bundle %s", url, resourceFileName )); - FileObject fileObject = URLMapper.findFileObject(url); - if(fileObject != null) { - if (fileObject.canWrite()) { - fileObject.addFileChangeListener( - WeakListeners.create(FileChangeListener.class, FILE_CHANGE_LISTENER, fileObject)); - LOGGER.finer(String.format("Added FileChangeListener to file %s", fileObject )); - } - } else { - LOGGER.fine(String.format("Cannot map %s URL to FileObject!", url)); - } - } - - java.util.ResourceBundle found = java.util.ResourceBundle.getBundle(bundleFile, Locale.getDefault(), classLoader); + found = java.util.ResourceBundle.getBundle(bundleFile, Locale.getDefault(), classLoader); result.put(bundleFile, new ResourceBundleInfo(bundle.getFiles(), found, bundle.getVar())); break; // found the bundle in source cp, skip searching compile cp } catch (MissingResourceException exception) { continue; } } - } } return result; } + private java.util.ResourceBundle loadBundleChain(List files) { + Locale defaultLocale = Locale.getDefault(); + String lang = defaultLocale.getLanguage(); + String country = defaultLocale.getCountry(); + + FileObject baseFile = null; + FileObject langFile = null; + FileObject countryFile = null; + FileObject fallbackFile = null; + + for (FileObject fo : files) { + String name = fo.getName(); + + if (fallbackFile == null) { + fallbackFile = fo; + } + + if (!name.contains("_")) { + baseFile = fo; + } else if (name.endsWith("_" + lang)) { + langFile = fo; + } else if (name.endsWith("_" + lang + "_" + country)) { + countryFile = fo; + } + } + java.util.ResourceBundle chainHead = null; + java.util.ResourceBundle currentParent = null; + if (baseFile != null) { + currentParent = createBundleSafe(baseFile); + chainHead = currentParent; + } + if (langFile != null) { + LinkableResourceBundle langBundle = createBundleSafe(langFile); + if (langBundle != null) { + if (currentParent != null) { + langBundle.setChainParent(currentParent); + } + currentParent = langBundle; + chainHead = langBundle; + } + } + if (countryFile != null) { + LinkableResourceBundle countryBundle = createBundleSafe(countryFile); + if (countryBundle != null) { + if (currentParent != null) { + countryBundle.setChainParent(currentParent); + } + chainHead = countryBundle; + } + } + if (chainHead == null && fallbackFile != null) { + return createBundleSafe(fallbackFile); + } + return chainHead; + } + + private LinkableResourceBundle createBundleSafe(FileObject fo) { + try (java.io.InputStream in = fo.getInputStream()) { + return new LinkableResourceBundle(in); + } catch (IOException ex) { + LOGGER.log(Level.FINE, "Error loading properties file: {0}", fo.getPath()); + return null; + } + } + + private static class LinkableResourceBundle extends PropertyResourceBundle { + + public LinkableResourceBundle(java.io.InputStream stream) throws IOException { + super(stream); + } + + public void setChainParent(java.util.ResourceBundle parent) { + setParent(parent); + } + } + private static final class ResourceBundleInfo { private final List files; private final java.util.ResourceBundle resourceBundle; @@ -477,10 +643,20 @@ public static class Location { private final int offset; private final FileObject file; + private final int lineNumber; + + public Location(FileObject file) { + this(0, file); + } public Location(int offset, FileObject file) { + this(offset, file, -1); + } + + public Location(int offset, FileObject file, int lineNumber) { this.offset = offset; this.file = file; + this.lineNumber = lineNumber; } public int getOffset() { @@ -490,5 +666,9 @@ public int getOffset() { public FileObject getFile() { return file; } + + public int getLineNumber() { + return lineNumber; + } } } diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCodeCompletionHandler.java b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCodeCompletionHandler.java index 2524ec80fb3d..cc9b3f3b50bb 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCodeCompletionHandler.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCodeCompletionHandler.java @@ -76,6 +76,7 @@ import org.netbeans.modules.web.el.spi.ELPlugin; import org.netbeans.modules.web.el.spi.ELVariableResolver.VariableInfo; import org.netbeans.modules.web.el.spi.Function; +import org.netbeans.modules.web.el.spi.ResolverContext; import org.netbeans.modules.web.el.spi.ResourceBundle; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; @@ -176,7 +177,7 @@ public void run(CompilationController info) throws Exception { // seems to be something like "sessionScope.^", so complete beans from the scope proposeBeansFromScope(ccontext, context, prefixMatcher, element, node, proposals); } else if (ELTypeUtilities.isResourceBundleVar(ccontext, node)) { - proposeBundleKeysInDotNotation(context, prefixMatcher, element, node, proposals); + proposeBundleKeysInDotNotation(ccontext, context, prefixMatcher, element, node, proposals); } else if (resolved == null) { if (target instanceof AstDotSuffix == false && node instanceof AstFunction == false) { proposeFunctions(ccontext, context, prefixMatcher, element, proposals); @@ -603,12 +604,13 @@ private void proposeBundleKeysInArrayNotation(CodeCompletionContext context, if (!resourceBundles.canHaveBundles()) { return; } + ResolverContext resolverContext = new ResolverContext(); FileObject bundleFile = null; - List bundleLocations = resourceBundles.getLocationsForBundleIdent(bundleKey); + List bundleLocations = resourceBundles.getLocationsForBundleIdent(resolverContext, bundleKey); if (!bundleLocations.isEmpty()) { bundleFile = bundleLocations.get(0).getFile(); } - for (Map.Entry entry : resourceBundles.getEntries(bundleKey).entrySet()) { + for (Map.Entry entry : resourceBundles.getEntries(resolverContext, bundleKey).entrySet()) { if (!prefix.matches(entry.getKey())) { continue; } @@ -620,7 +622,8 @@ private void proposeBundleKeysInArrayNotation(CodeCompletionContext context, } // "msg.key" notation - private void proposeBundleKeysInDotNotation(CodeCompletionContext context, + private void proposeBundleKeysInDotNotation(CompilationContext ccontext, + CodeCompletionContext context, PrefixMatcher prefix, ELElement elElement, Node baseObjectNode, @@ -632,11 +635,11 @@ private void proposeBundleKeysInDotNotation(CodeCompletionContext context, return; } FileObject bundleFile = null; - List bundleLocations = resourceBundles.getLocationsForBundleIdent(bundleKey); + List bundleLocations = resourceBundles.getLocationsForBundleIdent(ccontext.context(), bundleKey); if (!bundleLocations.isEmpty()) { bundleFile = bundleLocations.get(0).getFile(); } - for (Map.Entry entry : resourceBundles.getEntries(bundleKey).entrySet()) { + for (Map.Entry entry : resourceBundles.getEntries(ccontext.context(), bundleKey).entrySet()) { if (!prefix.matches(entry.getKey())) { continue; } diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCompletionUtil.java b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCompletionUtil.java new file mode 100644 index 000000000000..144a2e394478 --- /dev/null +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/ELCompletionUtil.java @@ -0,0 +1,31 @@ +/* + * 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.web.el.completion; + +import org.netbeans.modules.csl.api.ElementHandle; +import org.netbeans.modules.web.el.ELElement; +import org.openide.filesystems.FileObject; + +public class ELCompletionUtil { + + public static ElementHandle getResourceBundleElementHandle(String key, String value, ELElement element, FileObject bundleFile) { + return new ELResourceBundleKeyCompletionItem(key, value, element, bundleFile).getElement(); + } + +} diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesKey.svg b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesKey.svg new file mode 100644 index 000000000000..b81160dfce5e --- /dev/null +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesKey.svg @@ -0,0 +1,51 @@ + + + + + + α + + + a + + + \ No newline at end of file diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesLocale.svg b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesLocale.svg new file mode 100644 index 000000000000..e8702b9da09c --- /dev/null +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/completion/resources/propertiesLocale.svg @@ -0,0 +1,54 @@ + + + + + + α + + + a + + + + \ No newline at end of file diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/hints/ResourceBundleKeys.java b/enterprise/web.el/src/org/netbeans/modules/web/el/hints/ResourceBundleKeys.java index 76a1f2c7e750..1a3eb8f48bb1 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/hints/ResourceBundleKeys.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/hints/ResourceBundleKeys.java @@ -73,7 +73,7 @@ protected void run(CompilationContext info, RuleContext context, List resu } for (Pair pair : resourceBundles.collectKeys(each.getNode(), info.context())) { String clearedKey = pair.second().getImage().replace("'", "").replace("\"", ""); - if (!resourceBundles.isValidKey(pair.first().getImage(), clearedKey)) { + if (!resourceBundles.isValidKey(info.context(), pair.first().getImage(), clearedKey)) { Hint hint = new Hint(this, NbBundle.getMessage(ResourceBundleKeys.class, "ResourceBundleKeys_Unknown", clearedKey), elResult.getFileObject(), diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELDeclarationFinder.java b/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELDeclarationFinder.java index ce3dd96158ab..9305b7b24256 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELDeclarationFinder.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELDeclarationFinder.java @@ -41,7 +41,6 @@ import org.netbeans.api.java.source.Task; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; -import org.netbeans.modules.csl.api.DataLoadersBridge; import org.netbeans.modules.csl.api.DeclarationFinder; import org.netbeans.modules.csl.api.ElementKind; import org.netbeans.modules.csl.api.HtmlFormatter; @@ -54,6 +53,7 @@ import org.netbeans.modules.web.el.ELElement; import org.netbeans.modules.web.el.ELTypeUtilities; import org.netbeans.modules.web.el.ResourceBundles; +import org.netbeans.modules.web.el.completion.ELCompletionUtil; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; @@ -95,12 +95,13 @@ public void run(CompilationController cc) throws Exception { // resolve resource bundles ResourceBundles resourceBundles = ResourceBundles.get(file); if (resourceBundles.canHaveBundles()) { - List bundleLocations = getBundleLocations(resourceBundles, nodeElem); + List bundleLocations = getBundleLocations(context, resourceBundles, nodeElem); if (!bundleLocations.isEmpty()) { - refs.fo = bundleLocations.get(0).getFile(); - refs.offset = bundleLocations.get(0).getOffset(); - for (ResourceBundles.Location location : bundleLocations) { - alternatives.add(new ResourceBundleAlternative(location)); + refs.fo = bundleLocations.get(0).fo; + refs.offset = bundleLocations.get(0).offset; + refs.elementHandle = bundleLocations.get(0).elementHandle; + for (RefsHolder location : bundleLocations) { + alternatives.add(new ResourceBundleAlternative(location.fo, location.offset, location.lineNumber)); } } } @@ -128,7 +129,7 @@ public void run(CompilationController controller) throws Exception { } if (refs.fo != null && refs.offset != -1) { - DeclarationLocation declarationLocation = new DeclarationLocation(refs.fo, refs.offset); + DeclarationLocation declarationLocation = new DeclarationLocation(refs.fo, refs.offset, refs.elementHandle); for (AlternativeLocation alternativeLocation : alternatives) { declarationLocation.addAlternative(alternativeLocation); } @@ -152,9 +153,10 @@ public void run() { return ret.get(); } - private static List getBundleLocations(ResourceBundles resourceBundles, Pair nodeElem) { + private static List getBundleLocations(CompilationContext ccontext, ResourceBundles resourceBundles, Pair nodeElem) { if (nodeElem.first() instanceof AstIdentifier) { - return resourceBundles.getLocationsForBundleIdent(nodeElem.first().getImage()); + List locations = resourceBundles.getLocationsForBundleIdent(ccontext.context(), nodeElem.first().getImage()); + return locations.stream().map(location -> new RefsHolder(location.getFile(), location.getOffset())).toList(); } else { AstPath astPath = new AstPath(nodeElem.second().getNode()); for (Node node : astPath.rootToLeaf()) { @@ -165,21 +167,32 @@ private static List getBundleLocations(ResourceBundles // bundle['key'] image = image.substring(1, image.length() - 1); } - return resourceBundles.getLocationsForBundleKey(node.getImage(), image); + final String key = image; + List locations = resourceBundles.getLocationsForBundleKey(ccontext.context(), node.getImage(), key); + final String value; + if (!locations.isEmpty()) { + value = resourceBundles.getValueWithVarName(ccontext.context(), node.getImage(), key); + } else { + value = null; + } + return locations.stream().map(location -> new RefsHolder(location.getFile(), location.getOffset(), + ELCompletionUtil.getResourceBundleElementHandle(key, value, nodeElem.second(), location.getFile()), location.getLineNumber())).toList(); } } } - return Collections.emptyList(); + return Collections.emptyList(); } private static class ResourceBundleAlternative implements AlternativeLocation { private final FileObject file; private final int offset; + private final int lineNumber; - public ResourceBundleAlternative(ResourceBundles.Location location) { - this.offset = location.getOffset(); - this.file = location.getFile(); + public ResourceBundleAlternative(FileObject file, int offset, int lineNumber) { + this.offset = offset; + this.file = file; + this.lineNumber = lineNumber; } @Override @@ -189,13 +202,10 @@ public org.netbeans.modules.csl.api.ElementHandle getElement() { @Override public String getDisplayHtml(HtmlFormatter formatter) { - StringBuilder b = new StringBuilder(); - - b.append("");//NOI18N - b.append(""); //NOI18N - b.append(file.getName()); - b.append(""); //NOI18N - b.append(" in "); //NOI18N + formatter.emphasis(true); + formatter.appendText(file.getName()); + formatter.emphasis(false); + formatter.appendText(" in "); //add a link to the file relative to the web root FileObject pathRoot = ProjectWebRootQuery.getWebRoot(file); @@ -215,14 +225,14 @@ public String getDisplayHtml(HtmlFormatter formatter) { path = file.getPath(); } - b.append(""); //NOI18N - b.append(path); - b.append(""); //NOI18N - if (offset > 0) { - b.append(":"); //NOI18N - b.append(offset + 1); //line offsets are counted from zero, but in editor lines starts with one. + formatter.appendHtml(""); + formatter.appendText(path); + formatter.appendHtml(""); + if (lineNumber > -1) { + formatter.appendText(":"); //NOI18N + formatter.appendText(String.valueOf(lineNumber + 1)); //line numbers are counted from zero, but in editor lines starts with one. } - return b.toString(); + return formatter.getText(); } @Override @@ -274,9 +284,27 @@ public boolean equals(Object obj) { private static class RefsHolder { - private ElementHandle handle; - private FileObject fo; - private int offset = -1; + ElementHandle handle; + FileObject fo; + int offset = -1; + int lineNumber = -1; + org.netbeans.modules.csl.api.ElementHandle elementHandle; + + RefsHolder() { + } + + RefsHolder(FileObject fo, int offset) { + this.fo = fo; + this.offset = offset; + } + + public RefsHolder(FileObject fo, int offset, org.netbeans.modules.csl.api.ElementHandle elementHandle, int lineNumber) { + this.fo = fo; + this.offset = offset; + this.elementHandle = elementHandle; + this.lineNumber = lineNumber; + } + } private static class ResourceBundleElementHandle implements org.netbeans.modules.csl.api.ElementHandle { diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELHyperlinkProvider.java b/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELHyperlinkProvider.java index ca46365d37d1..6cd1611a572e 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELHyperlinkProvider.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/navigation/ELHyperlinkProvider.java @@ -84,7 +84,7 @@ public final class ELHyperlinkProvider implements HyperlinkProviderExt { @Override - public boolean isHyperlinkPoint(final Document doc, final int offset, HyperlinkType type) { + public boolean isHyperlinkPoint(final Document doc, final int offset, HyperlinkType type) { final AtomicBoolean ret = new AtomicBoolean(false); doc.render(new Runnable() { @@ -130,12 +130,14 @@ public Set getSupportedHyperlinkTypes() { public String getTooltipText(Document doc, int offset, HyperlinkType type) { Pair nodeAndElement = resolveNodeAndElement(doc, offset, new AtomicBoolean()); if (nodeAndElement != null) { - if (nodeAndElement.first() instanceof AstString) { + if (nodeAndElement.first() instanceof AstString || nodeAndElement.first() instanceof AstDotSuffix) { // could be a resource bundle key - return getTooltipTextForBundleKey(nodeAndElement); - } else { - return getTooltipTextForElement(nodeAndElement); + String tooltip = getTooltipTextForBundleKey(nodeAndElement); + if (tooltip != null) { + return tooltip; + } } + return getTooltipTextForElement(nodeAndElement); } return null; } diff --git a/enterprise/web.el/src/org/netbeans/modules/web/el/spi/ResourceBundle.java b/enterprise/web.el/src/org/netbeans/modules/web/el/spi/ResourceBundle.java index 94cef123a217..a72317b69d23 100644 --- a/enterprise/web.el/src/org/netbeans/modules/web/el/spi/ResourceBundle.java +++ b/enterprise/web.el/src/org/netbeans/modules/web/el/spi/ResourceBundle.java @@ -25,23 +25,23 @@ * @author marekfukala */ public final class ResourceBundle { - + private final String baseName, var; private final List files; - + public ResourceBundle(String baseName, String var, List files) { this.baseName = baseName; this.var = var; this.files = files; } - + /** * @return fully qualified name of the properties file representing the resource bundle. */ public String getBaseName() { return baseName; } - + /** * @return variable representing the resource bundle */ @@ -56,4 +56,9 @@ public List getFiles() { return files; } + @Override + public String toString() { + return "ResourceBundle{" + "baseName=" + baseName + ", var=" + var + '}'; + } + } diff --git a/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesKey.svg b/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesKey.svg new file mode 100644 index 000000000000..b81160dfce5e --- /dev/null +++ b/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesKey.svg @@ -0,0 +1,51 @@ + + + + + + α + + + a + + + \ No newline at end of file diff --git a/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesLocale.svg b/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesLocale.svg new file mode 100644 index 000000000000..e8702b9da09c --- /dev/null +++ b/enterprise/web.jsf.editor/src/org/netbeans/modules/web/jsf/editor/resources/propertiesLocale.svg @@ -0,0 +1,54 @@ + + + + + + α + + + a + + + + \ No newline at end of file diff --git a/enterprise/web.jsf/src/org/netbeans/modules/web/jsf/api/editor/JSFResourceBundlesProvider.java b/enterprise/web.jsf/src/org/netbeans/modules/web/jsf/api/editor/JSFResourceBundlesProvider.java index bccd8a430f8b..33bac43df6d4 100644 --- a/enterprise/web.jsf/src/org/netbeans/modules/web/jsf/api/editor/JSFResourceBundlesProvider.java +++ b/enterprise/web.jsf/src/org/netbeans/modules/web/jsf/api/editor/JSFResourceBundlesProvider.java @@ -22,9 +22,12 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import org.netbeans.api.java.classpath.ClassPath; import org.netbeans.api.java.project.JavaProjectConstants; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; @@ -53,29 +56,43 @@ public static List getResourceBundles(final Project project) { try { return model.runReadAction(metadata -> { List applications = metadata.getElements(Application.class); + + if (applications.isEmpty()) { + return Collections.emptyList(); + } + + List allSourceGroups = new ArrayList<>(); + Collections.addAll(allSourceGroups, SourceGroups.getJavaSourceGroups(project)); + Collections.addAll(allSourceGroups, ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES)); + + ClassPath compileClassPath = null; + if (!allSourceGroups.isEmpty()) { + compileClassPath = ClassPath.getClassPath(allSourceGroups.get(0).getRootFolder(), ClassPath.COMPILE); + } + List result = new ArrayList<>(); for (Application application : applications) { for (org.netbeans.modules.web.jsf.api.facesmodel.ResourceBundle bundle : application.getResourceBundles()) { - if (bundle.getBaseName() == null) { + String baseName = bundle.getBaseName(); + if (baseName == null) { continue; } - List files = new ArrayList<>(); - // java source source groups - for (SourceGroup sourceGroup : SourceGroups.getJavaSourceGroups(project)) { - FileObject bundleFile = getBundleFileInSourceGroup(sourceGroup, bundle); - if (bundleFile != null) { - files.add(bundleFile); - } + Set fileSet = new LinkedHashSet<>(); + int lastDelim = baseName.lastIndexOf("."); + String bundleSimpleName = (lastDelim <= 0) ? baseName : baseName.substring(lastDelim + 1); + String packagePath = (lastDelim <= 0) ? "" : baseName.replace(".", "/").substring(0, lastDelim); + + for (SourceGroup sourceGroup : allSourceGroups) { + FileObject root = sourceGroup.getRootFolder(); + FileObject folder = (lastDelim <= 0) ? root : root.getFileObject(packagePath); + addBundleFilesInFolder(fileSet, folder, bundleSimpleName); } - // resource source groups - for (SourceGroup sourceGroup : ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_RESOURCES)) { - FileObject bundleFile = getBundleFileInSourceGroup(sourceGroup, bundle); - if (bundleFile != null) { - files.add(bundleFile); - } + + if (fileSet.isEmpty() && compileClassPath != null) { + addBundleFilesInCompileClasspath(fileSet, compileClassPath, bundleSimpleName, packagePath); } - - result.add(new ResourceBundle(bundle.getBaseName(), bundle.getVar(), files)); + + result.add(new ResourceBundle(bundle.getBaseName(), bundle.getVar(), new ArrayList<>(fileSet))); } } return result; @@ -86,28 +103,22 @@ public static List getResourceBundles(final Project project) { return Collections.emptyList(); } - private static FileObject getBundleFileInSourceGroup(SourceGroup sourceGroup, org.netbeans.modules.web.jsf.api.facesmodel.ResourceBundle bundle) { - int lastDelim = bundle.getBaseName().lastIndexOf("/"); //NOI18N - String bundleName = bundle.getBaseName().substring(lastDelim + 1); - if (lastDelim <= 0) { - // in root folder or default package - return getBundleInFolder(sourceGroup.getRootFolder(), bundleName); - } else { - // in the subfolder or non-default package - String parentFolder = bundle.getBaseName().replace(".", "/").substring(0, lastDelim); //NOI18N - return getBundleInFolder(sourceGroup.getRootFolder().getFileObject(parentFolder), bundleName); - } - } - - private static FileObject getBundleInFolder(FileObject folder, String bundleName) { + private static void addBundleFilesInFolder(Set files, FileObject folder, String bundleName) { if (folder != null && folder.isValid() && folder.isFolder()) { for (FileObject fo : folder.getChildren()) { - if (fo.getName().startsWith(bundleName) && "properties".equals(fo.getExt())) { //NOI18N - return fo; + if ("properties".equals(fo.getExt()) + && (fo.getName().equals(bundleName) || fo.getName().startsWith(bundleName + "_"))) { + files.add(fo); } } } - return null; + } + + private static void addBundleFilesInCompileClasspath(Set files, ClassPath cp, String bundleName, String packagePath) { + List folders = cp.findAllResources(packagePath); + for (FileObject folder : folders) { + addBundleFilesInFolder(files, folder, bundleName); + } } } diff --git a/ide/properties/src/org/netbeans/modules/properties/propertiesKey.svg b/ide/properties/src/org/netbeans/modules/properties/propertiesKey.svg new file mode 100644 index 000000000000..b81160dfce5e --- /dev/null +++ b/ide/properties/src/org/netbeans/modules/properties/propertiesKey.svg @@ -0,0 +1,51 @@ + + + + + + α + + + a + + + \ No newline at end of file diff --git a/ide/properties/src/org/netbeans/modules/properties/propertiesLocale.svg b/ide/properties/src/org/netbeans/modules/properties/propertiesLocale.svg new file mode 100644 index 000000000000..e8702b9da09c --- /dev/null +++ b/ide/properties/src/org/netbeans/modules/properties/propertiesLocale.svg @@ -0,0 +1,54 @@ + + + + + + α + + + a + + + + \ No newline at end of file