001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.search;
043:
044: import java.io.FileNotFoundException;
045: import java.io.IOException;
046: import java.io.InputStream;
047: import java.io.InputStreamReader;
048: import java.io.LineNumberReader;
049: import java.nio.charset.Charset;
050: import java.util.ArrayList;
051: import java.util.Collection;
052: import java.util.HashMap;
053: import java.util.HashSet;
054: import java.util.List;
055: import java.util.Map;
056: import java.util.logging.Logger;
057: import java.util.regex.Matcher;
058: import java.util.regex.Pattern;
059: import java.util.regex.PatternSyntaxException;
060: import javax.swing.event.ChangeEvent;
061: import javax.swing.event.ChangeListener;
062: import org.netbeans.api.queries.FileEncodingQuery;
063: import org.openide.ErrorManager;
064: import org.openide.filesystems.FileObject;
065: import org.openide.loaders.DataObject;
066: import org.openide.nodes.Node;
067: import org.openidex.search.SearchPattern;
068: import static java.util.logging.Level.FINER;
069: import static java.util.logging.Level.FINEST;
070:
071: /**
072: * Class encapsulating basic search criteria.
073: *
074: * @author Marian Petras
075: */
076: final class BasicSearchCriteria {
077:
078: private static int instanceCounter;
079: private final int instanceId = instanceCounter++;
080: private static final Logger LOG = Logger
081: .getLogger("org.netbeans.modules.search.BasicSearchCriteria"); //NOI18N
082:
083: /** array of searchable application/x-<em>suffix</em> MIME-type suffixes */
084: private static final Collection<String> searchableXMimeTypes;
085:
086: static {
087: searchableXMimeTypes = new HashSet<String>(17);
088: searchableXMimeTypes.add("csh"); //NOI18N
089: searchableXMimeTypes.add("httpd-eruby"); //NOI18N
090: searchableXMimeTypes.add("httpd-php"); //NOI18N
091: searchableXMimeTypes.add("httpd-php-source"); //NOI18N
092: searchableXMimeTypes.add("javascript"); //NOI18N
093: searchableXMimeTypes.add("latex"); //NOI18N
094: searchableXMimeTypes.add("php"); //NOI18N
095: searchableXMimeTypes.add("sh"); //NOI18N
096: searchableXMimeTypes.add("tcl"); //NOI18N
097: searchableXMimeTypes.add("tex"); //NOI18N
098: searchableXMimeTypes.add("texinfo"); //NOI18N
099: searchableXMimeTypes.add("troff"); //NOI18N
100: }
101:
102: private String textPatternExpr;
103: private String fileNamePatternExpr;
104: private String replaceExpr;
105: private boolean wholeWords;
106: private boolean caseSensitive;
107: private boolean regexp;
108:
109: private boolean textPatternSpecified = false;
110: private boolean fileNamePatternSpecified = false;
111:
112: private boolean textPatternValid = false;
113: private boolean fileNamePatternValid = false;
114:
115: private Pattern textPattern;
116: private Pattern fileNamePattern;
117:
118: private boolean criteriaUsable = false;
119:
120: private ChangeListener usabilityChangeListener;
121:
122: /**
123: * Holds information about occurences of matching strings within individual
124: * {@code DataObject}s.
125: */
126: private Map<DataObject, List<TextDetail>> detailsMap;
127:
128: BasicSearchCriteria() {
129: if (LOG.isLoggable(FINER)) {
130: LOG.finer("#" + instanceId + ": <init>()"); //NOI18N
131: }
132: }
133:
134: /**
135: * Copy-constructor.
136: *
137: * @param template template to create a copy from
138: */
139: BasicSearchCriteria(BasicSearchCriteria template) {
140: if (LOG.isLoggable(FINER)) {
141: LOG.finer("#" + instanceId + ": <init>(template)"); //NOI18N
142: }
143:
144: /* check-boxes: */
145: setCaseSensitive(template.caseSensitive);
146: setWholeWords(template.wholeWords);
147: setRegexp(template.regexp);
148:
149: /* combo-boxes: */
150: setTextPattern(template.textPatternExpr);
151: setFileNamePattern(template.fileNamePatternExpr);
152: setReplaceString(template.replaceExpr);
153: }
154:
155: /**
156: * Returns a {@link Pattern} object corresponding to the substring pattern
157: * specified in the criteria.
158: *
159: * @return {@code Pattern} object, or {@code null} if no pattern has been
160: * specified
161: */
162: Pattern getTextPattern() {
163: if (!textPatternValid) {
164: return null;
165: }
166:
167: if (textPattern != null) {
168: return textPattern;
169: }
170:
171: /* So now we know that the pattern is valid but not compiled. */
172: if (regexp) {
173: assert false;//valid pattern for a regexp should be already compiled
174: textPatternValid = compileRegexpPattern();
175: } else {
176: compileSimpleTextPattern();
177: textPatternValid = (textPattern != null);
178: }
179: assert textPattern != null;
180: return textPattern; //may be null in case of invalid pattern
181: }
182:
183: String getTextPatternExpr() {
184: return textPatternExpr != null ? textPatternExpr : ""; //NOI18N
185: }
186:
187: /**
188: * Sets a text pattern. Whether it is considered a simple pattern or
189: * a regexp pattern, is determined by the current <em>regexp</em> setting
190: * (see {@link #setRegexp(boolean)}).
191: *
192: * @param pattern pattern to be set
193: */
194: void setTextPattern(String pattern) {
195: if (LOG.isLoggable(FINER)) {
196: LOG.finer("setTextPattern(" + pattern + ')'); //NOI18N
197: }
198: if ((pattern != null) && (pattern.length() == 0)) {
199: pattern = null;
200: }
201: if ((pattern == null) && (textPatternExpr == null)
202: || (pattern != null) && pattern.equals(textPatternExpr)) {
203: LOG.finest(" - no change"); //NOI18N
204: return;
205: }
206:
207: if (pattern == null) {
208: textPatternExpr = null;
209: textPattern = null;
210: textPatternSpecified = false;
211: textPatternValid = false;
212: } else {
213: textPatternExpr = pattern;
214: textPatternSpecified = true;
215: if (!regexp) {
216: textPattern = null;
217: textPatternValid = true;
218: } else {
219: textPatternValid = compileRegexpPattern();
220: assert (textPattern != null) || !textPatternValid;
221: }
222: }
223: updateUsability();
224: }
225:
226: /**
227: * Tries to compile the regular expression pattern, thus checking its
228: * validity. In case of success, the compiled pattern is stored
229: * to {@link #textPattern}, otherwise the field is set to {@code null}.
230: *
231: * @return {@code true} if the regexp pattern expression was valid;
232: * {@code false} otherwise
233: */
234: private boolean compileRegexpPattern() {
235: if (LOG.isLoggable(FINER)) {
236: LOG.finer("#" + instanceId + ": compileRegexpPattern()"); //NOI18N
237: }
238: assert regexp;
239: assert textPatternExpr != null;
240: try {
241: if (LOG.isLoggable(FINEST)) {
242: LOG.finest(" - textPatternExpr = \"" + textPatternExpr
243: + '"'); //NOI18N
244: }
245: textPattern = Pattern.compile(textPatternExpr);
246: return true;
247: } catch (PatternSyntaxException ex) {
248: LOG
249: .finest(" - invalid regexp - setting 'textPattern' to <null>"); //NOI18N
250: textPattern = null;
251: return false;
252: }
253: }
254:
255: /**
256: * Translates the simple text pattern to a regular expression pattern
257: * and compiles it. The compiled pattern is stored to field
258: * {@link #textPattern}.
259: */
260: private void compileSimpleTextPattern() {
261: if (LOG.isLoggable(FINER)) {
262: LOG.finer("#" + instanceId + ": compileRegexpPattern()"); //NOI18N
263: }
264: assert !regexp;
265: assert textPatternExpr != null;
266: try {
267: int flags = 0;
268: if (!caseSensitive) {
269: flags |= Pattern.CASE_INSENSITIVE;
270: }
271: if (LOG.isLoggable(FINEST)) {
272: LOG.finest(" - textPatternExpr = \"" + textPatternExpr
273: + '"'); //NOI18N
274: }
275: String searchRegexp = RegexpMaker.makeRegexp(
276: textPatternExpr, wholeWords);
277: if (LOG.isLoggable(FINEST)) {
278: LOG.finest(" - regexp = \"" + searchRegexp + '"'); //NOI18N
279: }
280: textPattern = Pattern.compile(searchRegexp, flags);
281: } catch (PatternSyntaxException ex) {
282: LOG.finest(" - invalid regexp"); //NOI18N
283: assert false;
284: textPattern = null;
285: }
286: }
287:
288: boolean isRegexp() {
289: return regexp;
290: }
291:
292: void setRegexp(boolean regexp) {
293: if (LOG.isLoggable(FINER)) {
294: LOG.finer("setRegexp(" + regexp + ')'); //NOI18N
295: }
296: if (regexp == this .regexp) {
297: LOG.finest(" - no change"); //NOI18N
298: return;
299: }
300:
301: this .regexp = regexp;
302:
303: if (textPatternExpr != null) {
304: if (regexp) {
305: textPatternValid = compileRegexpPattern();
306: } else {
307: textPatternValid = true;
308: textPattern = null;
309: }
310: }
311: updateUsability();
312: }
313:
314: boolean isWholeWords() {
315: return wholeWords;
316: }
317:
318: void setWholeWords(boolean wholeWords) {
319: if (LOG.isLoggable(FINER)) {
320: LOG.finer("setWholeWords(" + wholeWords + ')'); //NOI18N
321: }
322: if (wholeWords == this .wholeWords) {
323: LOG.finest(" - no change"); //NOI18N
324: return;
325: }
326:
327: this .wholeWords = wholeWords;
328:
329: if (!regexp) {
330: textPattern = null;
331: }
332: }
333:
334: boolean isCaseSensitive() {
335: return caseSensitive;
336: }
337:
338: void setCaseSensitive(boolean caseSensitive) {
339: if (LOG.isLoggable(FINER)) {
340: LOG.finer("setCaseSensitive(" + caseSensitive + ')'); //NOI18N
341: }
342: if (caseSensitive == this .caseSensitive) {
343: LOG.finest(" - no change"); //NOI18N
344: return;
345: }
346:
347: this .caseSensitive = caseSensitive;
348:
349: if (!regexp) {
350: textPattern = null;
351: }
352: }
353:
354: boolean isFullText() {
355: return textPatternValid;
356: }
357:
358: //--------------------------------------------------------------------------
359:
360: /**
361: * Returns a {@link Pattern} object corresponding to the file name pattern
362: * or set of patterns specified.
363: *
364: * @return {@code Pattern} object, or {@code null} if no pattern has been
365: * specified
366: */
367: Pattern getFileNamePattern() {
368: if (!fileNamePatternValid) {
369: return null;
370: }
371:
372: assert (fileNamePatternExpr != null)
373: && (fileNamePatternExpr.length() != 0);
374:
375: if (fileNamePattern != null) {
376: return fileNamePattern;
377: }
378:
379: /* So now we know that the pattern is valid but not compiled. */
380: compileSimpleFileNamePattern();
381: assert fileNamePattern != null;
382: return fileNamePattern;
383: }
384:
385: String getFileNamePatternExpr() {
386: return fileNamePatternExpr != null ? fileNamePatternExpr : ""; //NOI18N
387: }
388:
389: void setFileNamePattern(String pattern) {
390: if ((pattern != null) && (pattern.length() == 0)) {
391: pattern = null;
392: }
393: if ((pattern == null) && (fileNamePatternExpr == null)
394: || (pattern != null)
395: && pattern.equals(fileNamePatternExpr)) {
396: return;
397: }
398:
399: if (pattern == null) {
400: fileNamePatternExpr = null;
401: fileNamePattern = null;
402: fileNamePatternSpecified = false;
403: fileNamePatternValid = false;
404: } else {
405: fileNamePatternExpr = pattern;
406: fileNamePattern = null;
407: fileNamePatternSpecified = checkFileNamePattern(fileNamePatternExpr);
408: fileNamePatternValid = fileNamePatternSpecified;
409: }
410: updateUsability();
411: }
412:
413: /**
414: * Translates the simple text pattern to a regular expression pattern
415: * and compiles it. The compiled pattern is stored to field
416: * {@link #textPattern}.
417: */
418: private void compileSimpleFileNamePattern() {
419: assert fileNamePatternExpr != null;
420: try {
421: fileNamePattern = Pattern.compile(RegexpMaker
422: .makeMultiRegexp(fileNamePatternExpr),
423: Pattern.CASE_INSENSITIVE);
424: } catch (PatternSyntaxException ex) {
425: assert false;
426: fileNamePattern = null;
427: }
428: }
429:
430: /**
431: * Checks validity of the given file name pattern.
432: * The pattern is claimed to be valid if it contains at least one
433: * non-separator character. Separator characters are {@code ' '} (space)
434: * and {@code ','} (comma).
435: *
436: * @param fileNamePatternExpr pattern to be checked
437: * @return {@code true} if the pattern is valid, {@code false} otherwise
438: */
439: private static boolean checkFileNamePattern(
440: String fileNamePatternExpr) {
441: if (fileNamePatternExpr.length() == 0) {
442: return false; //trivial case
443: }
444:
445: for (char c : fileNamePatternExpr.toCharArray()) {
446: if ((c != ',') && (c != ' ')) {
447: return true;
448: }
449: }
450: return false;
451: }
452:
453: //--------------------------------------------------------------------------
454:
455: boolean isSearchAndReplace() {
456: return replaceExpr != null;
457: }
458:
459: /**
460: * Returns the replacement string/expression.
461: *
462: * @return replace string, or {@code null} if no replace string has been
463: * specified
464: */
465: String getReplaceExpr() {
466: return replaceExpr;
467: }
468:
469: /**
470: * Sets a replacement string/expression.
471: *
472: * @param replaceString string to replace matches with, or {@code null}
473: * if no replacing should be performed
474: */
475: void setReplaceString(String replaceString) {
476: this .replaceExpr = replaceString;
477: }
478:
479: //--------------------------------------------------------------------------
480:
481: private void updateUsability() {
482: boolean wasUsable = criteriaUsable;
483: criteriaUsable = isUsable();
484: if (criteriaUsable != wasUsable) {
485: fireUsabilityChanged();
486: }
487: }
488:
489: boolean isUsable() {
490: return (textPatternSpecified || (!isSearchAndReplace() && fileNamePatternSpecified))
491: && !isInvalid();
492: }
493:
494: private boolean isInvalid() {
495: return isTextPatternInvalid() || isFileNamePatternInvalid();
496: }
497:
498: void setUsabilityChangeListener(ChangeListener l) {
499: this .usabilityChangeListener = l;
500: }
501:
502: private void fireUsabilityChanged() {
503: if (usabilityChangeListener != null) {
504: usabilityChangeListener.stateChanged(new ChangeEvent(this ));
505: }
506: }
507:
508: boolean isTextPatternUsable() {
509: return textPatternSpecified && textPatternValid;
510: }
511:
512: boolean isTextPatternInvalid() {
513: return textPatternSpecified && !textPatternValid;
514: }
515:
516: boolean isFileNamePatternUsable() {
517: return fileNamePatternSpecified && fileNamePatternValid;
518: }
519:
520: boolean isFileNamePatternInvalid() {
521: return fileNamePatternSpecified && !fileNamePatternValid;
522: }
523:
524: //--------------------------------------------------------------------------
525:
526: /**
527: * Called when the criteria in the Find dialog are confirmed by the user
528: * and the search is about to be started.
529: * Makes sure everything is ready for searching, e.g. regexp patterns
530: * are compiled.
531: */
532: void onOk() {
533: LOG.finer("onOk()"); //NOI18N
534: if (textPatternValid && (textPattern == null)) {
535: assert !regexp; //should have been already compiled
536: compileSimpleTextPattern();
537: }
538: if (fileNamePatternValid && (fileNamePattern == null)) {
539: compileSimpleFileNamePattern();
540: }
541:
542: assert !textPatternValid || (textPattern != null);
543: assert !fileNamePatternValid || (fileNamePattern != null);
544: }
545:
546: boolean matches(DataObject dataObj) {
547: if (!dataObj.isValid()) {
548: return false;
549: }
550:
551: FileObject fileObj = dataObj.getPrimaryFile();
552: if (fileObj.isFolder() || !fileObj.isValid()
553: || (isFullText() && !isTextFile(fileObj))) {
554: return false;
555: }
556:
557: /* Check the file name: */
558: if (fileNamePatternValid
559: && !fileNamePattern.matcher(fileObj.getNameExt())
560: .matches()) {
561: return false;
562: }
563:
564: /* Check the file's content: */
565: if (textPatternValid && !checkFileContent(fileObj, dataObj)) {
566: return false;
567: }
568:
569: return true;
570: }
571:
572: /**
573: * Checks whether the given file is a text file.
574: * The current implementation does the check by the file's MIME-type.
575: *
576: * @param fileObj file to be checked
577: * @return {@code true} if the file is a text file;
578: * {@code false} if it is a binary file
579: */
580: private static boolean isTextFile(FileObject fileObj) {
581: String mimeType = fileObj.getMIMEType();
582:
583: if (mimeType.equals("content/unknown") //NOI18N
584: || mimeType.startsWith("text/")) { //NOI18N
585: return true;
586: }
587:
588: if (mimeType.startsWith("application/")) { //NOI18N
589: final String subtype = mimeType.substring(12);
590: return subtype.equals("rtf") //NOI18N
591: || subtype.equals("sgml") //NOI18N
592: || subtype.startsWith("xml-") //NOI18N
593: || subtype.endsWith("+xml") //NOI18N
594: || subtype.startsWith("x-") //NOI18N
595: && searchableXMimeTypes.contains(subtype
596: .substring(2));
597: }
598:
599: return false;
600: }
601:
602: /**
603: * Checks whether the file's content matches the text pattern.
604: *
605: * @param fileObj file whose content is to be checked
606: * @param dataObj {@code DataObject} corresponding to the file
607: * @return {@code true} if the file contains at least one substring
608: * matching the pattern, {@code false} otherwise
609: */
610: private boolean checkFileContent(FileObject fileObj,
611: DataObject dataObj) {
612: boolean firstMatch = true;
613: SearchPattern searchPattern = null;
614: ArrayList<TextDetail> txtDetails = null;
615:
616: LineNumberReader reader = null;
617: try {
618: reader = getFileObjectReader(fileObj);
619:
620: String line;
621: while ((line = reader.readLine()) != null) {
622: Matcher matcher = textPattern.matcher(line);
623: while (matcher.find()) {
624: if (firstMatch) {
625: searchPattern = createSearchPattern();
626: txtDetails = new ArrayList<TextDetail>(5);
627: firstMatch = false;
628: }
629: TextDetail det = new TextDetail(dataObj,
630: searchPattern);
631: det.setLine(reader.getLineNumber());
632: det.setLineText(line);
633: int start = matcher.start();
634: int len = matcher.end() - start;
635: det.setColumn(start + 1);
636: det.setMarkLength(len);
637: txtDetails.add(det);
638: }
639: }
640: if (txtDetails != null) {
641: txtDetails.trimToSize();
642: getDetailsMap().put(dataObj, txtDetails);
643: return true;
644: } else {
645: return false;
646: }
647: } catch (FileNotFoundException fnfe) {
648: return false;
649: } catch (IOException ioe) {
650: ErrorManager.getDefault().notify(
651: ErrorManager.INFORMATIONAL, ioe);
652: return false;
653: } finally {
654: if (reader != null) {
655: try {
656: reader.close();
657: reader = null;
658: } catch (IOException ex) {
659: ErrorManager.getDefault().notify(
660: ErrorManager.INFORMATIONAL, ex);
661: }
662: }
663: }
664: }
665:
666: /**
667: *
668: * @exception java.io.FileNotFoundException
669: * if file determined by the {@code FileObject} does not exist
670: */
671: private LineNumberReader getFileObjectReader(FileObject fileObj)
672: throws FileNotFoundException {
673: InputStream is = fileObj.getInputStream();//throws FileNotFoundException
674: Charset charset = getCharset(fileObj);
675: return new LineNumberReader(new InputStreamReader(is, charset));
676: }
677:
678: static Charset getCharset(FileObject fileObj) {
679: return FileEncodingQuery.getEncoding(fileObj);
680: }
681:
682: /**
683: * @param resultObject <code>DataObject</code> to create the nodes for
684: * @return <code>DetailNode</code>s representing the matches,
685: * or <code>null</code> if no matching string is known for the
686: * specified object
687: * @see DetailNode
688: */
689: public Node[] getDetails(Object resultObject) {
690: List<TextDetail> details = getDetailsMap().get(resultObject);
691: if (details == null) {
692: return null;
693: }
694:
695: List<Node> detailNodes = new ArrayList<Node>(details.size());
696: for (TextDetail txtDetail : details) {
697: detailNodes.add(new TextDetail.DetailNode(txtDetail));
698: }
699:
700: return detailNodes.toArray(new Node[detailNodes.size()]);
701: }
702:
703: /** Gets details map. */
704: private Map<DataObject, List<TextDetail>> getDetailsMap() {
705: if (detailsMap != null) {
706: return detailsMap;
707: }
708:
709: synchronized (this ) {
710: if (detailsMap == null) {
711: detailsMap = new HashMap<DataObject, List<TextDetail>>(
712: 20);
713: }
714: }
715:
716: return detailsMap;
717: }
718:
719: /**
720: * @param node representing a <code>DataObject</code> with matches
721: * @return <code>DetailNode</code>s representing the matches,
722: * or <code>null</code> if the specified node does not represent
723: * a <code>DataObject</code> or if no matching string is known for
724: * the specified object
725: */
726: public Node[] getDetails(Node node) {
727: DataObject dataObject = node.getCookie(DataObject.class);
728:
729: if (dataObject == null) {
730: return null;
731: }
732:
733: return getDetails(dataObject);
734: }
735:
736: /**
737: */
738: public int getDetailsCount(Object resultObject) {
739: List<TextDetail> details = getDetailsMap().get(resultObject);
740: return (details != null) ? details.size() : 0;
741: }
742:
743: /**
744: */
745: public List<TextDetail> getTextDetails(Object resultObject) {
746: List<TextDetail> obtained = getDetailsMap().get(resultObject);
747: return (obtained != null) ? new ArrayList<TextDetail>(obtained)
748: : null;
749: }
750:
751: private SearchPattern createSearchPattern() {
752: return SearchPattern.create(textPatternExpr, wholeWords,
753: caseSensitive, regexp);
754: }
755:
756: }
|