/******************************************************************************* * Copyright (c) 2000, 2008 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.wst.jsdt.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.filebuffers.FileBuffers; import org.eclipse.core.filebuffers.ITextFileBuffer; import org.eclipse.core.filebuffers.ITextFileBufferManager; import org.eclipse.core.filebuffers.LocationKind; import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jface.text.Position; import org.eclipse.search.ui.text.Match; import org.eclipse.wst.jsdt.core.IJavaScriptUnit; import org.eclipse.wst.jsdt.core.IField; import org.eclipse.wst.jsdt.core.IJavaScriptElement; import org.eclipse.wst.jsdt.core.ISourceReference; import org.eclipse.wst.jsdt.core.ToolFactory; import org.eclipse.wst.jsdt.core.compiler.IScanner; import org.eclipse.wst.jsdt.core.compiler.ITerminalSymbols; import org.eclipse.wst.jsdt.core.compiler.InvalidInputException; import org.eclipse.wst.jsdt.core.search.SearchMatch; import org.eclipse.wst.jsdt.core.search.SearchRequestor; import org.eclipse.wst.jsdt.internal.corext.refactoring.nls.PropertyFileDocumentModel; import org.eclipse.wst.jsdt.internal.corext.util.JavaModelUtil; import org.eclipse.wst.jsdt.internal.corext.util.Messages; import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin; import org.eclipse.wst.jsdt.internal.ui.util.StringMatcher; class NLSSearchResultRequestor extends SearchRequestor { /* * Matches are added to fResult. Element (group key) is IJavaScriptElement or FileEntry. */ private static final StringMatcher fgGetClassNameMatcher= new StringMatcher("*.class.getName()*", false, false); //$NON-NLS-1$ private NLSSearchResult fResult; private IFile fPropertiesFile; private Properties fProperties; private HashSet fUsedPropertyNames; public NLSSearchResultRequestor(IFile propertiesFile, NLSSearchResult result) { fPropertiesFile= propertiesFile; fResult= result; } /* * @see org.eclipse.wst.jsdt.core.search.SearchRequestor#beginReporting() */ public void beginReporting() { loadProperties(); fUsedPropertyNames= new HashSet(fProperties.size()); } /* * @see org.eclipse.wst.jsdt.core.search.SearchRequestor#acceptSearchMatch(org.eclipse.wst.jsdt.core.search.SearchMatch) */ 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 IJavaScriptElement)) return; IJavaScriptElement javaElement= (IJavaScriptElement) match.getElement(); // ignore matches in import declarations: if (javaElement.getElementType() == IJavaScriptElement.IMPORT_DECLARATION) return; if (javaElement.getElementType() == IJavaScriptElement.CLASS_FILE) return; //matches in import statements of class files if (javaElement.getElementType() == IJavaScriptElement.TYPE) return; //classes extending the accessor class and workaround for bug 61286 // heuristic: ignore matches in resource bundle name field: if (javaElement.getElementType() == IJavaScriptElement.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.indexOf("NLS.initializeMessages") != -1) //$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; IJavaScriptUnit[] allCompilationUnits= JavaModelUtil.getAllCompilationUnits(new IJavaScriptElement[] {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); String message= Messages.format(NLSSearchMessages.NLSSearchResultCollector_unusedKeys, getPropertiesName(fPropertiesFile)); FileEntry groupElement= new FileEntry(fPropertiesFile, message); 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 String getPropertiesName(IFile propertiesFile) { String path= propertiesFile.getFullPath().removeLastSegments(1).toOSString(); return propertiesFile.getName() + " - " + path; //$NON-NLS-1$ } 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.unwindEscapeChars(propertyName); 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 */ private boolean isKeyDefined(String key) { if (key == null) return true; // Parse error - don't check key 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 first argument and it is a string i.e. quoted ("..."). * * @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, null if no key can be found * @throws CoreException if a problem occurs while accessing the <code>enclosingElement</code> */ private String findKey(Position keyPositionResult, IJavaScriptElement enclosingElement) throws CoreException { IJavaScriptUnit unit= (IJavaScriptUnit)enclosingElement.getAncestor(IJavaScriptElement.JAVASCRIPT_UNIT); if (unit == null) return null; String source= unit.getSource(); if (source == null) return null; IScanner 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() != ITerminalSymbols.TokenNameIdentifier) return null; String src= new String(scanner.getCurrentTokenSource()); int keyStart= scanner.getCurrentTokenStartPosition(); int keyEnd= scanner.getCurrentTokenEndPosition(); if (scanner.getNextToken() == ITerminalSymbols.TokenNameLPAREN) { // Old school // next must be key string: if (scanner.getNextToken() != ITerminalSymbols.TokenNameStringLiteral) return null; // found it: keyStart= scanner.getCurrentTokenStartPosition() + 1; keyEnd= scanner.getCurrentTokenEndPosition(); keyPositionResult.setOffset(keyStart); keyPositionResult.setLength(keyEnd - keyStart); return source.substring(keyStart, keyEnd); } else { keyPositionResult.setOffset(keyStart); keyPositionResult.setLength(keyEnd - keyStart + 1); return src; } } catch (InvalidInputException e) { return null; } } /** * 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 JavaScriptPlugin.log(cex); return -1; } catch (IOException e) { if (stream != null) { try { stream.close(); } catch (IOException ce) { JavaScriptPlugin.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) { JavaScriptPlugin.log(ex); return -1; } finally { try { lineReader.close(); } catch (IOException ex) { JavaScriptPlugin.log(ex); } } return start; } private void loadProperties() { Set 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 duplicateKeys) { if (duplicateKeys.size() == 0) return; String message= Messages.format(NLSSearchMessages.NLSSearchResultCollector_duplicateKeys, getPropertiesName(fPropertiesFile)); FileEntry groupElement= new FileEntry(fPropertiesFile, message); Iterator iter= duplicateKeys.iterator(); while (iter.hasNext()) { String propertyName= (String) iter.next(); addMatch(groupElement, propertyName); } fResult.addFileEntryGroup(groupElement); } }