content: /*******************************************************************************
   * 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);
  	}
  
  }
  