blob: 5b6ef8586e995bc268d9152b259a4c809ea8a691 [file] [log] [blame]
/*
* 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.tasklist.todo;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.modules.tasklist.todo.settings.Settings;
import org.netbeans.modules.tasklist.todo.settings.ToDoOptionsController;
import org.netbeans.spi.tasklist.FileTaskScanner;
import org.netbeans.spi.tasklist.Task;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.NbBundle;
/**
*
* @author S. Aubrecht
* @author Tor Norbye
* @author Trond Norbye
*/
public class TodoTaskScanner extends FileTaskScanner implements PropertyChangeListener {
private static final String GROUP_NAME = "nb-tasklist-todo"; //NOI18N
private Pattern regexp = null;
private Callback callback;
/**
* Creates a new instance of TodoTaskProvider
*
*/
TodoTaskScanner( String displayName, String description ) {
super( displayName, description, "Team/" + ToDoOptionsController.OPTIONS_PATH); //NOI18N
}
public static TodoTaskScanner create() {
return new TodoTaskScanner( NbBundle.getBundle( TodoTaskScanner.class ).getString( "LBL_todotask" ), //NOI18N
NbBundle.getBundle( TodoTaskScanner.class ).getString( "HINT_todotask" ) ); //NOI18N
}
public List<? extends Task> scan( FileObject resource ) {
if( !isSupported( resource ) )
return null;
if( Settings.getDefault().isScanCommentsOnly() ) {
return scanComments( resource );
}
return scanAll( resource );
}
private List<? extends Task> scanAll( FileObject resource ) {
List<Task> tasks = null;
Collection<String> patterns = Settings.getDefault().getPatterns();
try {
String text = getContent( resource );
int index = 0;
int lineno = 1;
int len = text.length();
Matcher matcher = getScanRegexp().matcher( text );
while (index < len && matcher.find(index)) {
int begin = matcher.start();
int end = matcher.end();
// begin should be the beginning of this line (but avoid
// clash if I have two tokens on the same line...
char c = 'a'; // NOI18N
int nonwhite = begin;
while (begin >= index && (c = text.charAt(begin)) != '\n') { // NOI18N
if (c != ' ' && c != '\t') { // NOI18N
nonwhite = begin;
}
--begin;
}
begin = nonwhite;
// end should be the last "nonwhite" character on this line...
nonwhite = end;
while (end < len) {
c = text.charAt(end);
if (c == '\n' || c == '\r') {// NOI18N
break;
} else if (c != ' ' && c != '\t') {// NOI18N
nonwhite = end;
}
++end;
}
// calculate current line number
int idx = index;
while (idx <= begin) {
if (text.charAt(idx) == '\n') {// NOI18N
++lineno;
}
++idx;
}
index = end;
String description = new String( text.subSequence(begin, Math.min(nonwhite+1, text.length())).toString().toCharArray() );
description = trim( description, patterns );
Task task = Task.create( resource, GROUP_NAME, description, lineno );
if( null == tasks ) {
tasks = new LinkedList<Task>();
}
tasks.add( task );
}
} catch( IOException e ) {
Logger.getLogger( getClass().getName() ).log( Level.INFO, null, e );
} catch( OutOfMemoryError oomE ) {
System.gc();
Logger.getLogger( getClass().getName() ).log( Level.INFO, null, oomE );
}
return null == tasks ? getEmptyList() : tasks;
}
private List<? extends Task> scanComments( FileObject resource ) {
String ext = resource.getExt().toLowerCase();
String mime = FileUtil.getMIMEType( resource );
String lineComment = Settings.getDefault().getLineComment( ext, mime );
String blockCommentStart = Settings.getDefault().getBlockCommentStart( ext, mime );
String blockCommentEnd = Settings.getDefault().getBlockCommentEnd( ext, mime );
SourceCodeCommentParser sccp = new SourceCodeCommentParser( lineComment, blockCommentStart, blockCommentEnd );
Collection<String> patterns = Settings.getDefault().getPatterns();
List<Task> tasks = null;
try {
String text = getContent( resource );
sccp.setText( text );
SourceCodeCommentParser.CommentRegion reg = new SourceCodeCommentParser.CommentRegion();
Matcher matcher = getScanRegexp().matcher( text );
int len = text.length();
int lineno = 1;
int index = 0;
int idx = 0;
// find the first comment region
if (!sccp.nextRegion(reg)) {
// Done searching the document... bail out..
return getEmptyList();
}
while (index < len && matcher.find(index)) {
int begin = matcher.start();
int end = matcher.end();
boolean toosoon = false;
boolean goahead;
do {
goahead = true;
// A match within the source comment?
if (begin < reg.start) {
toosoon = true;
// too soon.. get next match
} else if (begin > reg.stop) {
goahead = false;
if (!sccp.nextRegion(reg)) {
// Done searching the document... bail out..
return null == tasks ? getEmptyList() : tasks;
}
}
} while (!goahead);
if (toosoon) {
// find next match!
index = end;
continue;
}
// begin should be the beginning of this line (but avoid
// clash if I have two tokens on the same line...
char c = 'a'; // NOI18N
int nonwhite = begin;
while (begin >= index && (c = text.charAt(begin)) != '\n') { // NOI18N
if (c != ' ' && c != '\t') { // NOI18N
nonwhite = begin;
}
--begin;
}
begin = nonwhite;
// end should be the last "nonwhite" character on this line...
nonwhite = end;
while (end < len) {
c = text.charAt(end);
if (c == '\n' || c == '\r') {// NOI18N
break;
} else if (c != ' ' && c != '\t') {// NOI18N
nonwhite = end;
}
++end;
}
// calculate current line number
while (idx <= begin) {
if (text.charAt(idx) == '\n') {// NOI18N
++lineno;
}
++idx;
}
index = end;
String description = new String( text.subSequence(begin, Math.min(nonwhite+1, text.length())).toString().toCharArray() );
description = trim( description, patterns );
Task task = Task.create( resource, GROUP_NAME, description, lineno );
if( null == tasks ) {
tasks = new LinkedList<Task>();
}
tasks.add( task );
}
} catch( IOException e ) {
Logger.getLogger( getClass().getName() ).log( Level.INFO, null, e );
} catch( OutOfMemoryError oomE ) {
System.gc();
Logger.getLogger( getClass().getName() ).log( Level.INFO, null, oomE );
}
return null == tasks ? getEmptyList() : tasks;
}
private boolean isSupported( FileObject file ) {
if( null == file || file.isFolder() )
return false;
return Settings.getDefault().isExtensionSupported( file.getExt() )
|| Settings.getDefault().isMimeTypeSupported( FileUtil.getMIMEType(file) );
}
Pattern getScanRegexp() {
// Create regexp from tags
if (regexp == null) {
StringBuffer sb = new StringBuffer(200);
Collection<String> patterns = Settings.getDefault().getPatterns();
boolean needSeparator = false;
for( String s : patterns ) {
if (s.isEmpty()) {
continue;
}
if( needSeparator ) {
sb.append('|');
}
needSeparator = true;
int n = s.length();
// Insert token/boundary separator when we're dealing
// with text tokens, since you probably don't want
// a todo-match in a comment like
// "and now process GLYPTODON content".
// However, for non-token tags, such as "<<<<" don't
// insert word boundary markers since it won't work - there's
// no word on the right...
if (Character.isJavaIdentifierPart(s.charAt(0))) {
// isJavaIdentifierPart - roughly matches what regex
// considers a word ([a-zA-Z_0-9])
// \W instead of \b: Workarond for issue 30250
sb.append("\\W"); // NOI18N
}
// "escape" the string here such that regexp meta
// characters are handled literally
for (int j = 0; j < n; j++) {
char c = s.charAt(j);
// regexp metachar?
if ((c == '(') || (c == ')') ||
(c == '{') || (c == '}') ||
(c == '[') || (c == ']') ||
(c == '?') || (c == '*') || (c == '+') ||
(c == '!') || (c == '|') || (c == '\\') ||
(c == '^') || (c == '$') || (c == '.')) {
sb.append('\\');
}
sb.append(c);
}
if (Character.isJavaIdentifierPart(s.charAt(n-1))) {
sb.append("\\b"); // NOI18N
}
}
try {
regexp = Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE);
} catch (PatternSyntaxException e) {
// Internal error: the regexp should have been validated when
// the user edited it
Logger.getLogger( getClass().getName() ).log( Level.INFO, null, e );
return null;
}
}
return regexp;
}
private String getContent( FileObject fileObject ) throws IOException {
InputStream input = null;
try {
input = fileObject.getInputStream();
} catch( FileNotFoundException fnfE ) {
//file was deleted
}
if( null == input )
return "";
char[] buf = new char[1024*64];
StringBuffer sb = new StringBuffer();
Charset charset = FileEncodingQuery.getEncoding( fileObject );
Reader r = new BufferedReader( new InputStreamReader( input, charset ) );
int len;
try {
while (true) {
len = r.read(buf);
if (len == -1) break;
sb.append(buf, 0, len);
}
} catch(IllegalArgumentException ex) { // reading of extra characters into the specified character buffer failed
Logger.getLogger(TodoTaskScanner.class.getName()).log(Level.INFO, "Reading failed for {0}, with charset: {1}.", new Object[]{FileUtil.toFile(fileObject).getPath(), charset}); //NOI18N;
r.close();
return "";
} finally {
r.close();
}
return sb.toString();
}
private List<? extends Task> getEmptyList() {
List<? extends Task> res = Collections.emptyList();
return res;
}
public void attach( Callback callback ) {
if( null == callback && null != this.callback ) {
regexp = null;
Settings.getDefault().removePropertyChangeListener( this );
} else if( null != callback && null == this.callback ) {
Settings.getDefault().addPropertyChangeListener( this );
}
this.callback = callback;
}
public void propertyChange( PropertyChangeEvent e ) {
if( Settings.PROP_PATTERN_LIST.equals( e.getPropertyName() )
|| Settings.PROP_SCAN_COMMENTS_ONLY.equals( e.getPropertyName() )
|| Settings.PROP_IDENTIFIERS_LIST.equals( e.getPropertyName() )){
regexp = null;
if( null != callback )
callback.refreshAll();
}
}
@Override
public void notifyPrepare() {
getScanRegexp();
}
@Override
public void notifyFinish() {
regexp = null;
}
private String trim(String comment, Collection<String> patterns) {
int index = Integer.MAX_VALUE;
for( String p : patterns ) {
int i = comment.toLowerCase().indexOf(p.toLowerCase());
if( i > 0 && i < index ) {
index = i;
}
}
if( index > 0 && index < Integer.MAX_VALUE )
comment = comment.substring(index);
return comment;
}
}