| /* |
| * 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; |
| |
| import com.sun.el.parser.AstBracketSuffix; |
| import com.sun.el.parser.AstDotSuffix; |
| import com.sun.el.parser.AstIdentifier; |
| 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.Logger; |
| import javax.swing.text.BadLocationException; |
| import javax.swing.text.StyledDocument; |
| import org.netbeans.api.java.classpath.ClassPath; |
| import org.netbeans.api.java.project.JavaProjectConstants; |
| import org.netbeans.api.project.FileOwnerQuery; |
| import org.netbeans.api.project.Project; |
| import org.netbeans.api.project.ProjectUtils; |
| import org.netbeans.api.project.SourceGroup; |
| import org.netbeans.api.project.Sources; |
| import org.netbeans.modules.web.api.webmodule.WebModule; |
| import org.netbeans.modules.web.el.spi.ELPlugin; |
| import org.netbeans.modules.web.el.spi.ResolverContext; |
| import org.netbeans.modules.web.el.spi.ResourceBundle; |
| import org.netbeans.spi.java.classpath.ClassPathProvider; |
| import org.openide.cookies.EditorCookie; |
| import org.openide.cookies.LineCookie; |
| import org.openide.filesystems.*; |
| import org.openide.loaders.DataObject; |
| import org.openide.loaders.DataObjectNotFoundException; |
| import org.openide.text.Line; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Pair; |
| import org.openide.util.Parameters; |
| import org.openide.util.WeakListeners; |
| |
| /** |
| * Helper class for dealing with (JSF) resource bundles. |
| * |
| * TODO: should define an SPI and have the JSF module (and others) implement it. |
| * Not urgent ATM as there would be just one impl anyway. |
| * |
| * |
| * @author Erno Mononen, mfukala@netbeans.org |
| */ |
| public final class ResourceBundles { |
| |
| private static final Logger LOGGER = Logger.getLogger(ResourceBundles.class.getName()); |
| /** |
| * Caches the bundles to avoid reading them again. Holds the bundles for |
| * one FileObject at time. |
| */ |
| protected static final Map<FileObject, ResourceBundles> CACHE = new WeakHashMap<>(1); |
| |
| private final WebModule webModule; |
| private final Project project; |
| |
| /* bundle base name to ResourceBundleInfo map */ |
| private Map<String, ResourceBundleInfo> bundlesMap; |
| private long currentBundlesHashCode; |
| |
| private final FileChangeListener FILE_CHANGE_LISTENER = new FileChangeAdapter() { |
| |
| @Override |
| public void fileChanged(FileEvent fe) { |
| super.fileChanged(fe); |
| LOGGER.finer(String.format("File %s has changed.", fe.getFile())); //NOI18N |
| resetResourceBundleMap(); |
| } |
| |
| }; |
| |
| private ResourceBundles(WebModule webModule, Project project) { |
| this.webModule = webModule; |
| this.project = project; |
| } |
| |
| public static ResourceBundles create(WebModule webModule, Project project) { |
| return new ResourceBundles(webModule, project); |
| } |
| |
| public static ResourceBundles get(FileObject fileObject) { |
| Parameters.notNull("fileObject", fileObject); |
| if (CACHE.containsKey(fileObject)) { |
| return CACHE.get(fileObject); |
| } else { |
| CACHE.clear(); |
| Project owner = FileOwnerQuery.getOwner(fileObject); |
| WebModule webModule = WebModule.getWebModule(fileObject); |
| ResourceBundles result = new ResourceBundles(webModule, owner); |
| CACHE.put(fileObject, result); |
| return result; |
| } |
| } |
| |
| public boolean canHaveBundles() { |
| return webModule != null && project != null; |
| } |
| |
| /** |
| * Checks whether the given {@code identifier} represents |
| * a base name of a resource bundle. |
| * @param identifier non-null identifier |
| * @param context non-null {@link ResolverContext} instance |
| * |
| * @return true if the given identifier represents a resource bundle |
| */ |
| public boolean isResourceBundleIdentifier(String identifier, ResolverContext context) { |
| Parameters.notNull("indentifier", identifier); |
| Parameters.notNull("context", context); |
| |
| for (ResourceBundle bundle : getBundles(context)) { |
| if (identifier.equals(bundle.getVar())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Checks whether the given {@code key} is defined in the given {@code bundle}. |
| * @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(String bundle, String key) { |
| ResourceBundleInfo rbInfo = getBundleForIdentifier(bundle); |
| if (rbInfo == null) { |
| // no matching bundle file |
| return true; |
| } |
| |
| // issue #231689 custom implementation of ResourceBundle |
| if (rbInfo.getResourceBundle().getKeys() == null) { |
| return true; |
| } |
| return rbInfo.getResourceBundle().containsKey(key); |
| } |
| |
| /** |
| * Gets bundle info for given identifier. |
| * @param ident identifier to examine |
| * @return resource bundle info if any found, {@code null} otherwise |
| */ |
| private ResourceBundleInfo getBundleForIdentifier(String ident) { |
| // XXX - do it more efficiently |
| for (Map.Entry<String, ResourceBundleInfo> entry : getBundlesMap().entrySet()) { |
| if (ident.equals(entry.getValue().getVarName())) { |
| return entry.getValue(); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Gets all locations for given bundle identifier. |
| * @param ident identifier of the bundle |
| * @return locations corresponding to given bundle name, never {@code null} |
| */ |
| public List<Location> getLocationsForBundleIdent(String ident) { |
| ResourceBundleInfo rbi = getBundleForIdentifier(ident); |
| if (rbi == null) { |
| return Collections.<Location>emptyList(); |
| } |
| |
| List<Location> locations = new ArrayList<>(rbi.getFiles().size()); |
| for (FileObject fileObject : rbi.getFiles()) { |
| locations.add(new Location(0, fileObject)); |
| } |
| return locations; |
| } |
| |
| /** |
| * Gets all locations for given bundle identifier and key. |
| * @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<Location> getLocationsForBundleKey(String ident, String key) { |
| List<Location> locations = new ArrayList<>(); |
| for (Location location : getLocationsForBundleIdent(ident)) { |
| try { |
| DataObject dobj = DataObject.find(location.getFile()); |
| EditorCookie ec = dobj.getLookup().lookup(EditorCookie.class); |
| try { |
| ec.openDocument(); |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| LineCookie lc = dobj.getLookup().lookup(LineCookie.class); |
| if (lc != null) { |
| Line.Set ls = lc.getLineSet(); |
| for (Line line : ls.getLines()) { |
| if (line.getText().contains(key + "=") || line.getText().contains(key + " =")) { //NOI18N |
| try { |
| StyledDocument document = ec.getDocument(); |
| int offset = document.getText(0, document.getLength()).indexOf(line.getText()); |
| locations.add(new Location(offset, location.getFile())); |
| } catch (BadLocationException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| } |
| } |
| } catch (DataObjectNotFoundException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| return locations; |
| } |
| |
| public List<Pair<AstIdentifier, Node>> collectKeys(final Node root) { |
| return collectKeys(root, new ResolverContext()); |
| } |
| |
| /** |
| * Collects references to resource bundle keys in the given {@code root}. |
| * @return List of identifier/string pairs. Identifier = resource bundle base name - string = res bundle key. |
| */ |
| public List<Pair<AstIdentifier, Node>> collectKeys(final Node root, ResolverContext context) { |
| final List<Pair<AstIdentifier, Node>> result = new ArrayList<>(); |
| List<Node> path = new AstPath(root).rootToLeaf(); |
| for (int i = 0; i < path.size(); i++) { |
| Node node = path.get(i); |
| if (node instanceof AstIdentifier && isResourceBundleIdentifier(node.getImage(), context)) { |
| // check for i18n["my.key"] => AST for that is: identifier, brackets and string |
| if (i + 2 < path.size()) { |
| Node brackets = path.get(i + 1); |
| Node string = path.get(i + 2); |
| if (brackets instanceof AstBracketSuffix && string instanceof AstString) { |
| result.add(Pair.of((AstIdentifier) node, string)); |
| } |
| } else if (i + 1 < path.size()) { |
| // check for bundle.key => AST for that is: identifier, dotSuffix |
| if (path.get(i + 1) instanceof AstDotSuffix) { |
| result.add(Pair.of((AstIdentifier) node, path.get(i + 1))); |
| } |
| } |
| } |
| } |
| return result; |
| } |
| |
| public String findResourceBundleIdentifier(AstPath astPath) { |
| List<Node> path = astPath.leafToRoot(); |
| for (int i = 0; i < path.size(); i++) { |
| Node node = path.get(i); |
| if (node instanceof AstString) { |
| // check for i18n["my.key"] => AST for that is: identifier, brackets and string - since |
| // we're searching from the leaf to root here, so the order |
| // is string, brackets and identifier |
| if (i + 2 < path.size()) { |
| Node brackets = path.get(i + 1); |
| Node identifier = path.get(i + 2); |
| if (brackets instanceof AstBracketSuffix |
| && identifier instanceof AstIdentifier |
| && isResourceBundleIdentifier(identifier.getImage(), new ResolverContext())) { |
| return identifier.getImage(); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| /** |
| * Gets the value of the given {@code key} in the given {@code bundle}. |
| * @param bundle the base name of the bundle. |
| * @param key key in the given bundle. |
| * @return the value or {@code null}. |
| */ |
| public String getValue(String bundle, String key) { |
| ResourceBundleInfo rbInfo = getBundlesMap().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 entries in the bundle identified by {@code bundleName}. |
| * @param bundleVar |
| * @return |
| */ |
| public Map<String,String> getEntries(String bundleVar) { |
| ResourceBundle bundle = findResourceBundleForVar(bundleVar); |
| ResourceBundleInfo rbInfo = getBundlesMap().get(bundle.getBaseName()); |
| if (rbInfo == null) { |
| return Collections.emptyMap(); |
| } |
| Map<String, String> result = new HashMap<>(); |
| for (String key : rbInfo.getResourceBundle().keySet()) { |
| String value = rbInfo.getResourceBundle().getString(key); |
| result.put(key, value); |
| } |
| return result; |
| } |
| |
| |
| private ResourceBundle findResourceBundleForVar(String variableName) { |
| List<ResourceBundle> foundBundles = webModule != null ? |
| ELPlugin.Query.getResourceBundles(webModule.getDocumentBase(), new ResolverContext()) |
| : |
| Collections.<ResourceBundle>emptyList(); |
| //make the bundle var to bundle |
| for(ResourceBundle b : foundBundles) { |
| if(variableName.equals(b.getVar())) { |
| return b; |
| } |
| } |
| return null; |
| } |
| /** |
| * Finds list of all ResourceBundles, which are registered in all |
| * JSF configuration files in a web module. |
| */ |
| public List<ResourceBundle> getBundles(ResolverContext context) { |
| FileObject docBase = webModule != null ? webModule.getDocumentBase() : null; |
| return docBase != null ? ELPlugin.Query.getResourceBundles(docBase, context) : Collections.<ResourceBundle>emptyList(); |
| } |
| |
| /* |
| * returns a map of bundle fully qualified name to java.util.ResourceBundle |
| */ |
| private synchronized Map<String, ResourceBundleInfo> getBundlesMap() { |
| long bundlesHash = getBundlesHashCode(); |
| if (bundlesMap == null) { |
| currentBundlesHashCode = bundlesHash; |
| bundlesMap = createResourceBundleMapAndFileChangeListeners(); |
| LOGGER.fine("New resource bundle map created."); //NOI18N |
| } else { |
| if(bundlesHash != currentBundlesHashCode) { |
| //refresh the resource bundle map |
| resetResourceBundleMap(); |
| bundlesMap = createResourceBundleMapAndFileChangeListeners(); |
| currentBundlesHashCode = bundlesHash; |
| LOGGER.fine("Resource bundle map recreated based on configuration changes."); //NOI18N |
| |
| } |
| } |
| |
| return bundlesMap; |
| } |
| |
| private synchronized void resetResourceBundleMap() { |
| if(bundlesMap == null) { |
| return ; |
| } |
| 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 |
| } |
| } |
| bundlesMap = null; |
| LOGGER.fine("Resource bundle map released."); //NOI18N |
| } |
| |
| private long getBundlesHashCode() { |
| //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())) { |
| hash = 11 * hash + rb.getBaseName().hashCode(); |
| hash = 11 * hash + (rb.getVar() != null ? rb.getVar().hashCode() : 0); |
| } |
| return hash; |
| } |
| |
| private Map<String, ResourceBundleInfo> createResourceBundleMapAndFileChangeListeners() { |
| Map<String, ResourceBundleInfo> result = new HashMap<>(); |
| ClassPathProvider provider = project.getLookup().lookup(ClassPathProvider.class); |
| if (provider == null) { |
| return null; |
| } |
| |
| Sources sources = ProjectUtils.getSources(project); |
| if (sources == null) { |
| return null; |
| } |
| |
| SourceGroup[] sourceGroups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); |
| for (ResourceBundle bundle : getBundles(new ResolverContext())) { |
| String bundleFile = bundle.getBaseName(); |
| for (SourceGroup sourceGroup : sourceGroups) { |
| FileObject rootFolder = sourceGroup.getRootFolder(); |
| |
| for (String classPathType : new String[]{ClassPath.SOURCE, ClassPath.COMPILE}) { |
| ClassPath classPath = ClassPath.getClassPath(rootFolder, classPathType); |
| if (classPath == null) { |
| continue; |
| } |
| 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); |
| 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 static final class ResourceBundleInfo { |
| private final List<FileObject> files; |
| private final java.util.ResourceBundle resourceBundle; |
| private final String varName; |
| |
| public ResourceBundleInfo(List<FileObject> files, java.util.ResourceBundle resourceBundle, String varName) { |
| this.files = files; |
| this.resourceBundle = resourceBundle; |
| this.varName = varName; |
| } |
| |
| public List<FileObject> getFiles() { |
| return files; |
| } |
| |
| public java.util.ResourceBundle getResourceBundle() { |
| return resourceBundle; |
| } |
| |
| public String getVarName() { |
| return varName; |
| } |
| } |
| |
| public static class Location { |
| |
| private final int offset; |
| private final FileObject file; |
| |
| public Location(int offset, FileObject file) { |
| this.offset = offset; |
| this.file = file; |
| } |
| |
| public int getOffset() { |
| return offset; |
| } |
| |
| public FileObject getFile() { |
| return file; |
| } |
| } |
| } |