/* * $Id: SourceDirectoryReader.java 1408 2009-04-14 16:06:46Z amandel $ * * Copyright 2006, The jCoderZ.org Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the jCoderZ.org Project nor the names of * its contributors may be used to endorse or promote products * derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jcoderz.phoenix.report; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.bind.JAXBException; import org.jcoderz.commons.util.Assert; import org.jcoderz.commons.util.Constants; import org.jcoderz.commons.util.IoUtil; import org.jcoderz.commons.util.JaxbUtil; import org.jcoderz.commons.util.ObjectUtil; import org.jcoderz.commons.util.StringUtil; import org.jcoderz.commons.util.JaxbUtil.UnmarshalResult; import org.jcoderz.phoenix.report.ftf.jaxb.FindingDescription; import org.jcoderz.phoenix.report.ftf.jaxb.FindingTypeFormat; import org.jcoderz.phoenix.report.jaxb.Item; import org.xml.sax.InputSource; /** * Reads reports with format definitions described in the * finding-type-format-definition.xds. * * To find the finding type format definition for requested format * the following locations are used: * * The name is converted to lower case. * * A file <i>name</i>.xml is searched in the * <code>org.jcoderz.phoenix.report.ftf</code> package. If * this is not found the file is searched in the <code>ftf</code> * directory. The directory must be available through the classpath. * * * @author Andreas Mandel * */ public final class GenericReportReader implements ReportReader { private static final int MAX_DEBUG_TEXT_CHARS = 100; private static final String CLASSNAME = GenericReportReader.class.getName(); private static final Logger logger = Logger.getLogger(CLASSNAME); private static final Pattern CODE_LINE_PATTERN = Pattern.compile("^.*$", Pattern.MULTILINE); private static final Pattern CARET_LINE_PATTERN = Pattern.compile("^\\s*\\^$", Pattern.MULTILINE); private static final Map<Origin, GenericReportReader> GENERIC_REPORT_TYPES = new HashMap<Origin, GenericReportReader>(); private final List<GenericFindingType> mFindingTypes = new ArrayList<GenericFindingType>(); private Map<ResourceInfo, List<Item>> mItems; private SourceFile mSourceFile; private final Pattern mMessagePattern; private final FindingTypeFormat mFindingTypeFormatDescription; private final int mTextPos; private final Origin mOrigin; private final int mFilePos; private final int mLineStart; private final Severity mDefaultSeverity; private Matcher mRootMatcher = null; private GenericReportReader (Origin type) throws JAXBException { mOrigin = type; mFindingTypeFormatDescription = loadFormatDescription(type); initializeFindingTypes(); final FindingDescription root = mFindingTypeFormatDescription.getRootType(); mMessagePattern = Pattern.compile(root.getPattern(), Pattern.MULTILINE); mTextPos = Integer.parseInt(root.getTextPos()); mFilePos = Integer.parseInt(root.getFilenamePos()); mLineStart = root.isSetLineStartPos() ? Integer.parseInt(root.getLineStartPos()) : -1; mDefaultSeverity = root.isSetSeverity() ? root.getSeverity() : Severity.CODE_STYLE; } /** * Initializes the selected finding type. * Might return <code>null</code> if the initialization fails. * CHECKME: Should return a null object? * @param findingType the type to load. * @return the loaded finding type. */ public static GenericReportReader initialize (Origin findingType) { GenericReportReader result = null; synchronized (GENERIC_REPORT_TYPES) { if (!GENERIC_REPORT_TYPES.containsKey(findingType)) { try { result = new GenericReportReader(findingType); } catch (Exception ex) { // TODO: collect this an add it to the findings map later! logger.log(Level.WARNING, "Could not load finding type for '" + findingType + "' failed with " + ex.getMessage() + ".", ex); } GENERIC_REPORT_TYPES.put(findingType, result); } result = GENERIC_REPORT_TYPES.get(findingType); } return result; } private static FindingTypeFormat loadFormatDescription (Origin type) throws JAXBException { FindingTypeFormat findingTypeFormatDescription = null; InputStream in = null; try { final String filename = type.toString().toLowerCase(Constants.SYSTEM_LOCALE) + ".xml"; in = GenericReportReader.class.getResourceAsStream( "ftf/" + filename); if (in == null) { in = GenericReportReader.class.getResourceAsStream( "/ftf/" + filename); } if (in == null) { try { in = new FileInputStream(filename); } catch (FileNotFoundException ex) { // in = null; } } Assert.notNull(in, "report type description " + type); final UnmarshalResult unmarshal = JaxbUtil.unmarshal(new InputSource(in), "org.jcoderz.phoenix.report.ftf.jaxb"); findingTypeFormatDescription = (FindingTypeFormat) unmarshal.getParsedData(); } finally { IoUtil.close(in); } return findingTypeFormatDescription; } /** {@inheritDoc} */ public void parse (File f) throws JAXBException { try { mSourceFile = new SourceFile(f); mRootMatcher = mMessagePattern.matcher(mSourceFile.getContent()); } catch (IOException ex) { throw new JAXBException("Failed to read '" + f + "'.", ex); } } /** {@inheritDoc} */ public void merge (Map<ResourceInfo, List<Item>> items) throws JAXBException { mItems = items; while (!mSourceFile.readFully()) { parseNext(); } } /** * Reads the given message and tries to find a matching finding type. * @param message the message to read. * @return the finding type matching to the message, or null if no such * type was found. * @throws JAXBException if item creation fails. */ public Item detectFindingTypeForMessage (String message) throws JAXBException { Item result = null; for (final GenericFindingType type : mFindingTypes) { result = type.createItem(mSourceFile, message); if (result != null) { if (type.isSourceColumnByCaret()) { addPositionByCaret(result); } break; } } if (logger.isLoggable(Level.FINE)) { logger.fine("For text: '" + StringUtil.trimLength(message, MAX_DEBUG_TEXT_CHARS) + "' matched finding: " + (result == null ? "null" : result.getFindingType() + "'. End at " + mSourceFile.getPos())); } return result; } private void addPositionByCaret (final Item i) { final String text = mSourceFile.getContent().substring(mSourceFile.getPos()); final Matcher codeMat = CODE_LINE_PATTERN.matcher(text); if (codeMat.lookingAt()) { final String textAfterCode = mSourceFile.getContent().substring( mSourceFile.getPos() + codeMat.end() + 1); final Matcher caretMat = CARET_LINE_PATTERN.matcher(textAfterCode); if (caretMat.lookingAt()) { i.setColumn(caretMat.end()); mSourceFile.setPos( mSourceFile.getPos() + codeMat.end() + 1 + caretMat.end() + 1); } else { logger.fine("Caret defined but not found for '" + i.getFindingType() + "' Code Line: '" + codeMat + "' caretLine: '" + caretMat + "'. text: '" + StringUtil.trimLength( textAfterCode, MAX_DEBUG_TEXT_CHARS) + "'."); } } else { logger.fine("Caret defined but not found for '" + i.getFindingType() + "' Code Line: '" + codeMat + "'. text: '" + StringUtil.trimLength( text, MAX_DEBUG_TEXT_CHARS) + "'."); } } private void parseNext () throws JAXBException { if (mRootMatcher.find()) { final String text = mRootMatcher.group(mTextPos); if (logger.isLoggable(Level.FINE)) { logger.fine("Main pattern matched for: '" + StringUtil.trimLength(text, MAX_DEBUG_TEXT_CHARS) + "'. End at " + mRootMatcher.end()); } mSourceFile.setPos(mRootMatcher.start(mTextPos)); final Item item = detectFindingTypeForMessage(text); if (item == null) { final int pos = mSourceFile.getContent().indexOf( '\n', mSourceFile.getPos()); if (pos != -1) { mSourceFile.setPos(pos + 1); } else { mSourceFile.setPos(mSourceFile.getContent().length()); } } else { item.setOrigin(mOrigin); if (!item.isSetSeverity()) { item.setSeverity(mDefaultSeverity); } if (!item.isSetLine() && mLineStart != -1 && mRootMatcher.group(mLineStart) != null) { item.setLine( Integer.parseInt(mRootMatcher.group(mLineStart))); } if (!item.isSetFindingType()) { item.setFindingType(mOrigin.toString()); } if (!item.isSetMessage()) { item.setMessage(mRootMatcher.group(mTextPos)); } if (mFindingTypeFormatDescription.getRootType().isGlobal()) { item.setGlobal(true); } addItemToResource(mRootMatcher.group(mFilePos), item); } mRootMatcher.region( mSourceFile.getPos(), mSourceFile.getContent().length()); } else { if (logger.isLoggable(Level.FINE)) { logger.fine("No match after " + mSourceFile.getPos() + " '" + StringUtil.trimLength( mSourceFile.getContent().substring( mSourceFile.getPos()), MAX_DEBUG_TEXT_CHARS)); } // set pos to end of file mSourceFile.setPos(mSourceFile.getContent().length()); } } private void addItemToResource (String resourceFilename, Item item) { final ResourceInfo info = ResourceInfo.lookup(resourceFilename); if (info != null || item.isGlobal()) { final List<Item> l; if (mItems.containsKey(info)) { l = mItems.get(info); } else { l = new ArrayList<Item>(); mItems.put(info, l); } // sometimes javadoc reports the same thing twice... final Iterator<Item> i = l.iterator(); while (i.hasNext()) { final Item it = i.next(); if (it.getLine() == item.getLine() && it.getColumn() == item.getColumn() && it.getOrigin() == item.getOrigin() && ObjectUtil.equals(it.getMessage(), item.getMessage())) { i.remove(); break; } } l.add(item); } else { logger.finer("Ignore findings for resource '" + resourceFilename + "' type was " + item.getFindingType() + "."); } } private void initializeFindingTypes () { final FindingDescription root = mFindingTypeFormatDescription.getRootType(); final List<FindingDescription> findingTypes = mFindingTypeFormatDescription.getFindingType(); for (FindingDescription findingDesc : findingTypes) { final GenericFindingType gft = new GenericFindingType(root, findingDesc); mFindingTypes.add(gft); } Collections.sort( mFindingTypes, new GenericFindingType.OrderByPriority()); } static final class SourceFile { private final File mFile; private final String mContent; private int mPos; public SourceFile (File file) throws IOException { mFile = file; Reader in = null; Reader buffered = null; try { in = new FileReader(file); buffered = new BufferedReader(in); mContent = IoUtil.readFullyNormalizeNewLine(buffered); } finally { IoUtil.close(buffered); IoUtil.close(in); } mPos = 0; } /** * @return the pos */ public int getPos () { return mPos; } /** * @param pos the pos to set */ public void setPos (int pos) { mPos = pos; } /** * @return the file */ public File getFile () { return mFile; } /** * @return the content */ public String getContent () { return mContent; } public boolean readFully () { return mPos >= mContent.length(); } } }