/*
 * 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.apache.jmeter.protocol.http.util.accesslog;

import java.io.Serializable;
import java.util.ArrayList;

import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import org.apache.oro.text.MalformedCachePatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;

// For JUnit tests, @see TestLogFilter

/**
 * Description:<br>
 * <br>
 * LogFilter is a basic implementation of Filter interface. This implementation
 * will keep a record of the filtered strings to avoid repeating the process
 * unnecessarily.
 * <p>
 * The current implementation supports replacing the file extension. The reason
 * for supporting this is from first hand experience porting an existing website
 * to Tomcat + JSP. Later on we may want to provide the ability to replace the
 * whole filename. If the need materializes, we can add it later.
 * <p>
 * Example of how to use it is provided in the main method. An example is
 * provided below.
 * <p>
 *
 * <pre>
 * testf = new LogFilter();
 * String[] incl = { &quot;hello.html&quot;, &quot;index.html&quot;, &quot;/index.jsp&quot; };
 * String[] thefiles = { &quot;/test/hello.jsp&quot;, &quot;/test/one/hello.html&quot;, &quot;hello.jsp&quot;, &quot;hello.htm&quot;, &quot;/test/open.jsp&quot;,
 *      &quot;/test/open.html&quot;, &quot;/index.jsp&quot;, &quot;/index.jhtml&quot;, &quot;newindex.jsp&quot;, &quot;oldindex.jsp&quot;, &quot;oldindex1.jsp&quot;,
 *      &quot;oldindex2.jsp&quot;, &quot;oldindex3.jsp&quot;, &quot;oldindex4.jsp&quot;, &quot;oldindex5.jsp&quot;, &quot;oldindex6.jsp&quot;, &quot;/test/index.htm&quot; };
 * testf.excludeFiles(incl);
 * System.out.println(&quot; ------------ exclude test -------------&quot;);
 * for (int idx = 0; idx &lt; thefiles.length; idx++) {
 *  boolean fl = testf.isFiltered(thefiles[idx]);
 *  String line = testf.filter(thefiles[idx]);
 *  if (line != null) {
 *     System.out.println(&quot;the file: &quot; + line);
 *  }
 * }
 * </pre>
 *
 * As a general note. Both isFiltered and filter() have to be called. Calling
 * either one will not produce the desired result. isFiltered(string) will tell
 * you if a string should be filtered. The second step is to filter the string,
 * which will return null if it is filtered and replace any part of the string
 * that should be replaced.
 * <p>
 *
 */

public class LogFilter implements Filter, Serializable {

    private static final long serialVersionUID = 240L;

    private static final Logger log = LoggingManager.getLoggerForClass();

    /** protected members used by class to filter * */
    protected boolean CHANGEEXT = false;

    protected String OLDEXT = null;

    protected String NEWEXT = null;

    protected String[] INCFILE = null;

    protected String[] EXCFILE = null;

    protected boolean FILEFILTER = false;

    protected boolean USEFILE = true;

    protected String[] INCPTRN = null;

    protected String[] EXCPTRN = null;

    protected boolean PTRNFILTER = false;

    protected ArrayList<Pattern> EXCPATTERNS = new ArrayList<Pattern>();

    protected ArrayList<Pattern> INCPATTERNS = new ArrayList<Pattern>();

    protected String NEWFILE = null;

    /**
     * The default constructor is empty
     */
    public LogFilter() {
        super();
    }

    /**
     * The method will replace the file extension with the new one. You can
     * either provide the extension without the period ".", or with. The method
     * will check for period and add it if it isn't present.
     *
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#setReplaceExtension(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public void setReplaceExtension(String oldext, String newext) {
        if (oldext != null && newext != null) {
            this.CHANGEEXT = true;
            if (oldext.indexOf('.') < 0 && newext.indexOf('.') < 0) {
                this.OLDEXT = "." + oldext;
                this.NEWEXT = "." + newext;
            } else {
                this.OLDEXT = oldext;
                this.NEWEXT = newext;
            }
        }
    }

    /**
     * Give the filter a list of files to include
     *
     * @param filenames
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#includeFiles(java.lang.String[])
     */
    @Override
    public void includeFiles(String[] filenames) {
        if (filenames != null && filenames.length > 0) {
            INCFILE = filenames;
            this.FILEFILTER = true;
        }
    }

    /**
     * Give the filter a list of files to exclude
     *
     * @param filenames
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#excludeFiles(java.lang.String[])
     */
    @Override
    public void excludeFiles(String[] filenames) {
        if (filenames != null && filenames.length > 0) {
            EXCFILE = filenames;
            this.FILEFILTER = true;
        }
    }

    /**
     * Give the filter a set of regular expressions to filter with for
     * inclusion. This method hasn't been fully implemented and test yet. The
     * implementation is not complete.
     *
     * @param regexp
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#includePattern(String[])
     */
    @Override
    public void includePattern(String[] regexp) {
        if (regexp != null && regexp.length > 0) {
            INCPTRN = regexp;
            this.PTRNFILTER = true;
            // now we create the compiled pattern and
            // add it to the arraylist
            for (int idx = 0; idx < INCPTRN.length; idx++) {
                this.INCPATTERNS.add(this.createPattern(INCPTRN[idx]));
            }
        }
    }

    /**
     * Give the filter a set of regular expressions to filter with for
     * exclusion. This method hasn't been fully implemented and test yet. The
     * implementation is not complete.
     *
     * @param regexp
     *
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#excludePattern(String[])
     */
    @Override
    public void excludePattern(String[] regexp) {
        if (regexp != null && regexp.length > 0) {
            EXCPTRN = regexp;
            this.PTRNFILTER = true;
            // now we create the compiled pattern and
            // add it to the arraylist
            for (int idx = 0; idx < EXCPTRN.length; idx++) {
                this.EXCPATTERNS.add(this.createPattern(EXCPTRN[idx]));
            }
        }
    }

    /**
     * In the case of log filtering the important thing is whether the log entry
     * should be used. Therefore, the method will only return true if the entry
     * should be used. Since the interface defines both inclusion and exclusion,
     * that means by default inclusion filtering assumes all entries are
     * excluded unless it matches. In the case of exlusion filtering, it assumes
     * all entries are included unless it matches, which means it should be
     * excluded.
     *
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#isFiltered(String, TestElement)
     * @param path
     * @return boolean
     */
    @Override
    public boolean isFiltered(String path,TestElement el) {
        // we do a quick check to see if any
        // filters are set. If not we just
        // return false to be efficient.
        if (this.FILEFILTER || this.PTRNFILTER || this.CHANGEEXT) {
            if (this.FILEFILTER) {
                return filterFile(path);
            } else if (this.PTRNFILTER) {
                return filterPattern(path);
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * Filter the file. The implementation performs the exclusion first before
     * the inclusion. This means if a file name is in both string arrays, the
     * exclusion will take priority. Depending on how users expect this to work,
     * we may want to change the priority so that inclusion is performed first
     * and exclusion second. Another possible alternative is to perform both
     * inclusion and exclusion. Doing so would make the most sense if the method
     * throws an exception and tells the user the same filename is in both the
     * include and exclude array.
     *
     * @param file
     * @return boolean
     */
    protected boolean filterFile(String file) {
        // double check this logic make sure it
        // makes sense
        if (this.EXCFILE != null) {
            return excFile(file);
        } else if (this.INCFILE != null) {
            return !incFile(file);
        }
        return false;
    }

    /**
     * Method implements the logic for filtering file name inclusion. The method
     * iterates through the array and uses indexOf. Once it finds a match, it
     * won't bother with the rest of the filenames in the array.
     *
     * @param text
     * @return boolean include
     */
    public boolean incFile(String text) {
        // inclusion filter assumes most of
        // the files are not wanted, therefore
        // usefile is set to false unless it
        // matches.
        this.USEFILE = false;
        for (int idx = 0; idx < this.INCFILE.length; idx++) {
            if (text.indexOf(this.INCFILE[idx]) > -1) {
                this.USEFILE = true;
                break;
            }
        }
        return this.USEFILE;
    }

    /**
     * Method implements the logic for filtering file name exclusion. The method
     * iterates through the array and uses indexOf. Once it finds a match, it
     * won't bother with the rest of the filenames in the array.
     *
     * @param text
     * @return boolean exclude
     */
    public boolean excFile(String text) {
        // exclusion filter assumes most of
        // the files are used, therefore
        // usefile is set to true, unless
        // it matches.
        this.USEFILE = true;
        boolean exc = false;
        for (int idx = 0; idx < this.EXCFILE.length; idx++) {
            if (text.indexOf(this.EXCFILE[idx]) > -1) {
                exc = true;
                this.USEFILE = false;
                break;
            }
        }
        return exc;
    }

    /**
     * The current implemenation assumes the user has checked the regular
     * expressions so that they don't cancel each other. The basic assumption is
     * the method will return true if the text should be filtered. If not, it
     * will return false, which means it should not be filtered.
     *
     * @param text
     * @return boolean
     */
    protected boolean filterPattern(String text) {
        if (this.INCPTRN != null) {
            return !incPattern(text);
        } else if (this.EXCPTRN != null) {
            return excPattern(text);
        }
        return false;
    }

    /**
     * By default, the method assumes the entry is not included, unless it
     * matches. In that case, it will return true.
     *
     * @param text
     * @return true if text is included
     */
    protected boolean incPattern(String text) {
        this.USEFILE = false;
        for (int idx = 0; idx < this.INCPATTERNS.size(); idx++) {
            if (JMeterUtils.getMatcher().contains(text, this.INCPATTERNS.get(idx))) {
                this.USEFILE = true;
                break;
            }
        }
        return this.USEFILE;
    }

    /**
     * The method assumes by default the text is not excluded. If the text
     * matches the pattern, it will then return true.
     *
     * @param text
     * @return true if text is excluded
     */
    protected boolean excPattern(String text) {
        this.USEFILE = true;
        boolean exc = false;
        for (int idx = 0; idx < this.EXCPATTERNS.size(); idx++) {
            if (JMeterUtils.getMatcher().contains(text, this.EXCPATTERNS.get(idx))) {
                exc = true;
                this.USEFILE = false;
                break;
            }
        }
        return exc;
    }

    /**
     * Method uses indexOf to replace the old extension with the new extesion.
     * It might be good to use regular expression, but for now this is a simple
     * method. The method isn't designed to replace multiple instances of the
     * text, since that isn't how file extensions work. If the string contains
     * more than one instance of the old extension, only the first instance will
     * be replaced.
     *
     * @param text
     * @return boolean
     */
    public boolean replaceExtension(String text) {
        int pt = text.indexOf(this.OLDEXT);
        if (pt > -1) {
            int extsize = this.OLDEXT.length();
            this.NEWFILE = text.substring(0, pt) + this.NEWEXT + text.substring(pt + extsize);
            return true;
        } else {
            return false;
        }
    }

    /**
     * The current implementation checks the boolean if the text should be used
     * or not. isFilter( string) has to be called first.
     *
     * @see org.apache.jmeter.protocol.http.util.accesslog.Filter#filter(java.lang.String)
     */
    @Override
    public String filter(String text) {
        if (this.CHANGEEXT) {
            if (replaceExtension(text)) {
                return this.NEWFILE;
            } else {
                return text;
            }
        } else if (this.USEFILE) {
            return text;
        } else {
            return null;
        }
    }

    /**
     * create a new pattern object from the string.
     *
     * @param pattern
     * @return Pattern
     */
    public Pattern createPattern(String pattern) {
        try {
            return JMeterUtils.getPatternCache().getPattern(pattern,
                    Perl5Compiler.READ_ONLY_MASK | Perl5Compiler.SINGLELINE_MASK);
        } catch (MalformedCachePatternException exception) {
            log.error("Problem with pattern: "+pattern,exception);
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void reset() {

    }
}
