| /******************************************************************************* |
| * Copyright (c) 2000, 2013 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.ui.refactoring.nls.search; |
| |
| import java.io.BufferedInputStream; |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| |
| import org.eclipse.core.resources.IFile; |
| |
| import org.eclipse.core.filebuffers.FileBuffers; |
| import org.eclipse.core.filebuffers.ITextFileBuffer; |
| import org.eclipse.core.filebuffers.ITextFileBufferManager; |
| import org.eclipse.core.filebuffers.LocationKind; |
| |
| import org.eclipse.jface.text.Position; |
| |
| import org.eclipse.search.ui.text.Match; |
| |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.IField; |
| import org.eclipse.jdt.core.IJavaElement; |
| import org.eclipse.jdt.core.IJavaProject; |
| import org.eclipse.jdt.core.ISourceReference; |
| import org.eclipse.jdt.core.IType; |
| import org.eclipse.jdt.core.JavaCore; |
| import org.eclipse.jdt.core.Signature; |
| import org.eclipse.jdt.core.ToolFactory; |
| import org.eclipse.jdt.core.compiler.IScanner; |
| import org.eclipse.jdt.core.compiler.ITerminalSymbols; |
| import org.eclipse.jdt.core.compiler.InvalidInputException; |
| import org.eclipse.jdt.core.search.SearchMatch; |
| import org.eclipse.jdt.core.search.SearchRequestor; |
| |
| import org.eclipse.jdt.internal.corext.refactoring.nls.PropertyFileDocumentModel; |
| import org.eclipse.jdt.internal.corext.util.JavaModelUtil; |
| |
| import org.eclipse.jdt.internal.ui.JavaPlugin; |
| import org.eclipse.jdt.internal.ui.JavaUIStatus; |
| import org.eclipse.jdt.internal.ui.util.StringMatcher; |
| |
| |
| class NLSSearchResultRequestor extends SearchRequestor { |
| |
| /** |
| * Warning-free alias for <code>ITerminalSymbols.TokenNameIdentifier</code>. |
| */ |
| @SuppressWarnings("deprecation") |
| protected static final int InternalTokenNameIdentifier= ITerminalSymbols.TokenNameIdentifier; |
| |
| /* |
| * Matches are added to fResult. Element (group key) is IJavaElement or FileEntry. |
| */ |
| |
| private static final StringMatcher fgGetClassNameMatcher= new StringMatcher("*.class.getName()*", false, false); //$NON-NLS-1$ |
| |
| /** |
| * Object to indicate that no key has been found. |
| * @see #findKey(Position, IJavaElement) |
| * @since 3.6 |
| */ |
| private static final String NO_KEY= ""; //$NON-NLS-1$ |
| |
| private NLSSearchResult fResult; |
| private IFile fPropertiesFile; |
| private Properties fProperties; |
| private HashSet<String> fUsedPropertyNames; |
| |
| public NLSSearchResultRequestor(IFile propertiesFile, NLSSearchResult result) { |
| fPropertiesFile= propertiesFile; |
| fResult= result; |
| } |
| |
| /* |
| * @see org.eclipse.jdt.core.search.SearchRequestor#beginReporting() |
| */ |
| @Override |
| public void beginReporting() { |
| loadProperties(); |
| fUsedPropertyNames= new HashSet<>(fProperties.size()); |
| } |
| |
| /* |
| * @see org.eclipse.jdt.core.search.SearchRequestor#acceptSearchMatch(org.eclipse.jdt.core.search.SearchMatch) |
| */ |
| @Override |
| public void acceptSearchMatch(SearchMatch match) throws CoreException { |
| if (match.getAccuracy() == SearchMatch.A_INACCURATE) |
| return; |
| |
| int offset= match.getOffset(); |
| int length= match.getLength(); |
| if (offset == -1 || length == -1) |
| return; |
| |
| if (! (match.getElement() instanceof IJavaElement)) |
| return; |
| IJavaElement javaElement= (IJavaElement) match.getElement(); |
| |
| // ignore matches in import declarations: |
| if (javaElement.getElementType() == IJavaElement.IMPORT_DECLARATION) |
| return; |
| if (javaElement.getElementType() == IJavaElement.CLASS_FILE) |
| return; //matches in import statements of class files |
| if (javaElement.getElementType() == IJavaElement.TYPE) |
| return; //classes extending the accessor class and workaround for bug 61286 |
| |
| // heuristic: ignore matches in resource bundle name field: |
| if (javaElement.getElementType() == IJavaElement.FIELD) { |
| IField field= (IField) javaElement; |
| String source= field.getSource(); |
| if (source != null && fgGetClassNameMatcher.match(source)) |
| return; |
| } |
| |
| if (javaElement instanceof ISourceReference) { |
| String source= ((ISourceReference) javaElement).getSource(); |
| if (source != null) { |
| if (source.contains("NLS.initializeMessages")) //$NON-NLS-1$ |
| return; |
| } |
| } |
| |
| // found reference to NLS Wrapper - now check if the key is there: |
| Position mutableKeyPosition= new Position(offset, length); |
| //TODO: What to do if argument string not found? Currently adds a match with type name. |
| String key= findKey(mutableKeyPosition, javaElement); |
| if (key == null || isKeyDefined(key)) |
| return; |
| |
| ICompilationUnit[] allCompilationUnits= JavaModelUtil.getAllCompilationUnits(new IJavaElement[] {javaElement}); |
| Object element= javaElement; |
| if (allCompilationUnits != null && allCompilationUnits.length == 1) |
| element= allCompilationUnits[0]; |
| |
| fResult.addMatch(new Match(element, mutableKeyPosition.getOffset(), mutableKeyPosition.getLength())); |
| } |
| |
| public void reportUnusedPropertyNames(IProgressMonitor pm) { |
| //Don't use endReporting() for long running operation. |
| pm.beginTask("", fProperties.size()); //$NON-NLS-1$ |
| boolean hasUnused= false; |
| pm.setTaskName(NLSSearchMessages.NLSSearchResultRequestor_searching); |
| FileEntry groupElement= new FileEntry(fPropertiesFile, NLSSearchMessages.NLSSearchResultCollector_unusedKeys); |
| |
| for (Enumeration<?> enumeration= fProperties.propertyNames(); enumeration.hasMoreElements();) { |
| String propertyName= (String) enumeration.nextElement(); |
| if (!fUsedPropertyNames.contains(propertyName)) { |
| addMatch(groupElement, propertyName); |
| hasUnused= true; |
| } |
| pm.worked(1); |
| } |
| if (hasUnused) |
| fResult.addFileEntryGroup(groupElement); |
| pm.done(); |
| } |
| |
| |
| |
| private void addMatch(FileEntry groupElement, String propertyName) { |
| /* |
| * TODO (bug 63794): Should read in .properties file with our own reader and not |
| * with Properties.load(InputStream) . Then, we can remember start position and |
| * original version (not interpreting escape characters) for each property. |
| * |
| * The current workaround is to escape the key again before searching in the |
| * .properties file. However, this can fail if the key is escaped in a different |
| * manner than what PropertyFileDocumentModel.unwindEscapeChars(.) produces. |
| */ |
| String escapedPropertyName= PropertyFileDocumentModel.escape(propertyName, false); |
| int start= findPropertyNameStartPosition(escapedPropertyName); |
| int length; |
| if (start == -1) { // not found -> report at beginning |
| start= 0; |
| length= 0; |
| } else { |
| length= escapedPropertyName.length(); |
| } |
| fResult.addMatch(new Match(groupElement, start, length)); |
| } |
| |
| /** |
| * Checks if the key is defined in the property file |
| * and adds it to the list of used properties. |
| * |
| * @param key the key |
| * @return <code>true</code> if the key is defined, <code>false</code> otherwise |
| */ |
| private boolean isKeyDefined(String key) { |
| if (NO_KEY.equals(key)) |
| return false; |
| |
| fUsedPropertyNames.add(key); |
| if (fProperties.getProperty(key) != null) { |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean hasPropertyKey(String key) { |
| return fProperties.containsKey(key); |
| } |
| |
| public boolean isUsedPropertyKey(String key) { |
| return fUsedPropertyNames.contains(key); |
| } |
| |
| /** |
| * Finds the key defined by the given match. The assumption is that the key is the only argument |
| * and it is a string literal i.e. quoted ("...") or a string constant i.e. 'static final |
| * String' defined in the same class. |
| * |
| * @param keyPositionResult reference parameter: will be filled with the position of the found |
| * key |
| * @param enclosingElement enclosing java element |
| * @return a string denoting the key, {@link #NO_KEY} if no key can be found and |
| * <code>null</code> otherwise |
| * @throws CoreException if a problem occurs while accessing the <code>enclosingElement</code> |
| */ |
| private String findKey(Position keyPositionResult, IJavaElement enclosingElement) throws CoreException { |
| ICompilationUnit unit= (ICompilationUnit)enclosingElement.getAncestor(IJavaElement.COMPILATION_UNIT); |
| if (unit == null) |
| return null; |
| |
| String source= unit.getSource(); |
| if (source == null) |
| return null; |
| |
| IJavaProject javaProject= unit.getJavaProject(); |
| IScanner scanner= null; |
| if (javaProject != null) { |
| String complianceLevel= javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true); |
| String sourceLevel= javaProject.getOption(JavaCore.COMPILER_SOURCE, true); |
| scanner= ToolFactory.createScanner(false, false, false, sourceLevel, complianceLevel); |
| } else { |
| scanner= ToolFactory.createScanner(false, false, false, false); |
| } |
| scanner.setSource(source.toCharArray()); |
| scanner.resetTo(keyPositionResult.getOffset() + keyPositionResult.getLength(), source.length()); |
| |
| try { |
| if (scanner.getNextToken() != ITerminalSymbols.TokenNameDOT) |
| return null; |
| |
| if (scanner.getNextToken() != InternalTokenNameIdentifier) // assuming that unit is not module-info.java |
| return null; |
| |
| String src= new String(scanner.getCurrentTokenSource()); |
| int tokenStart= scanner.getCurrentTokenStartPosition(); |
| int tokenEnd= scanner.getCurrentTokenEndPosition(); |
| |
| if (scanner.getNextToken() == ITerminalSymbols.TokenNameLPAREN) { |
| // Old school |
| // next must be key string. Ignore methods which do not take a single String parameter (Bug 295040). |
| int nextToken= scanner.getNextToken(); |
| if (nextToken != ITerminalSymbols.TokenNameStringLiteral && nextToken != InternalTokenNameIdentifier) |
| return null; |
| |
| tokenStart= scanner.getCurrentTokenStartPosition(); |
| tokenEnd= scanner.getCurrentTokenEndPosition(); |
| int token; |
| while ((token= scanner.getNextToken()) == ITerminalSymbols.TokenNameDOT) { |
| if ((nextToken= scanner.getNextToken()) != InternalTokenNameIdentifier) { |
| return null; |
| } |
| tokenStart= scanner.getCurrentTokenStartPosition(); |
| tokenEnd= scanner.getCurrentTokenEndPosition(); |
| } |
| if (token != ITerminalSymbols.TokenNameRPAREN) |
| return null; |
| |
| if (nextToken == ITerminalSymbols.TokenNameStringLiteral) { |
| keyPositionResult.setOffset(tokenStart + 1); |
| keyPositionResult.setLength(tokenEnd - tokenStart - 1); |
| return source.substring(tokenStart + 1, tokenEnd); |
| } else if (nextToken == InternalTokenNameIdentifier) { |
| keyPositionResult.setOffset(tokenStart); |
| keyPositionResult.setLength(tokenEnd - tokenStart + 1); |
| IType parentClass= (IType)enclosingElement.getAncestor(IJavaElement.TYPE); |
| String identifier= source.substring(tokenStart, tokenEnd + 1); |
| for (IField field : parentClass.getFields()) { |
| if (field.getElementName().equals(identifier)) { |
| if (!"String".equals(Signature.getSignatureSimpleName(field.getTypeSignature()))) { //$NON-NLS-1$ |
| return null; |
| } |
| Object obj= field.getConstant(); |
| return obj instanceof String ? ((String)obj).substring(1, ((String)obj).length() - 1) : NO_KEY; |
| } |
| } |
| } |
| return NO_KEY; |
| } else { |
| IJavaElement[] keys= unit.codeSelect(tokenStart, tokenEnd - tokenStart + 1); |
| |
| // an interface can't be a key |
| if (keys.length == 1 && keys[0].getElementType() == IJavaElement.TYPE && ((IType) keys[0]).isInterface()) |
| return null; |
| |
| keyPositionResult.setOffset(tokenStart); |
| keyPositionResult.setLength(tokenEnd - tokenStart + 1); |
| return src; |
| } |
| } catch (InvalidInputException e) { |
| throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, e)); |
| } |
| } |
| |
| /** |
| * Finds the start position in the property file. We assume that |
| * the key is the first match on a line. |
| * |
| * @param propertyName the property name |
| * @return the start position of the property name in the file, -1 if not found |
| */ |
| private int findPropertyNameStartPosition(String propertyName) { |
| // Fix for http://dev.eclipse.org/bugs/show_bug.cgi?id=19319 |
| InputStream stream= null; |
| LineReader lineReader= null; |
| String encoding; |
| try { |
| encoding= fPropertiesFile.getCharset(); |
| } catch (CoreException e1) { |
| encoding= "ISO-8859-1"; //$NON-NLS-1$ |
| } |
| try { |
| stream= createInputStream(fPropertiesFile); |
| lineReader= new LineReader(stream, encoding); |
| } catch (CoreException cex) { |
| // failed to get input stream |
| JavaPlugin.log(cex); |
| return -1; |
| } catch (IOException e) { |
| if (stream != null) { |
| try { |
| stream.close(); |
| } catch (IOException ce) { |
| JavaPlugin.log(ce); |
| } |
| } |
| return -1; |
| } |
| int start= 0; |
| try { |
| StringBuffer buf= new StringBuffer(80); |
| int eols= lineReader.readLine(buf); |
| int keyLength= propertyName.length(); |
| while (eols > 0) { |
| String line= buf.toString(); |
| int i= line.indexOf(propertyName); |
| int charPos= i + keyLength; |
| char terminatorChar= 0; |
| boolean hasNoValue= (charPos >= line.length()); |
| if (i > -1 && !hasNoValue) |
| terminatorChar= line.charAt(charPos); |
| if (line.trim().startsWith(propertyName) && |
| (hasNoValue || Character.isWhitespace(terminatorChar) || terminatorChar == '=')) { |
| start += line.indexOf(propertyName); |
| eols= -17; // found key |
| } else { |
| start += line.length() + eols; |
| buf.setLength(0); |
| eols= lineReader.readLine(buf); |
| } |
| } |
| if (eols != -17) |
| start= -1; //key not found in file. See bug 63794. This can happen if the key contains escaped characters. |
| } catch (IOException ex) { |
| JavaPlugin.log(ex); |
| return -1; |
| } finally { |
| try { |
| lineReader.close(); |
| } catch (IOException ex) { |
| JavaPlugin.log(ex); |
| } |
| } |
| return start; |
| } |
| |
| private void loadProperties() { |
| Set<Object> duplicateKeys= new HashSet<>(); |
| fProperties= new Properties(duplicateKeys); |
| InputStream stream; |
| try { |
| stream= new BufferedInputStream(createInputStream(fPropertiesFile)); |
| } catch (CoreException ex) { |
| fProperties= new Properties(); |
| return; |
| } |
| try { |
| fProperties.load(stream); |
| } catch (IOException ex) { |
| fProperties= new Properties(); |
| return; |
| } finally { |
| try { |
| stream.close(); |
| } catch (IOException ex) { |
| } |
| reportDuplicateKeys(duplicateKeys); |
| } |
| } |
| |
| private InputStream createInputStream(IFile propertiesFile) throws CoreException { |
| ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager(); |
| if (manager != null) { |
| ITextFileBuffer buffer= manager.getTextFileBuffer(propertiesFile.getFullPath(), LocationKind.IFILE); |
| if (buffer != null) { |
| return new ByteArrayInputStream(buffer.getDocument().get().getBytes()); |
| } |
| } |
| |
| return propertiesFile.getContents(); |
| } |
| |
| private void reportDuplicateKeys(Set<Object> duplicateKeys) { |
| if (duplicateKeys.isEmpty()) |
| return; |
| |
| FileEntry groupElement= new FileEntry(fPropertiesFile, NLSSearchMessages.NLSSearchResultCollector_duplicateKeys); |
| Iterator<Object> iter= duplicateKeys.iterator(); |
| while (iter.hasNext()) { |
| String propertyName= (String) iter.next(); |
| addMatch(groupElement, propertyName); |
| } |
| fResult.addFileEntryGroup(groupElement); |
| } |
| |
| } |