/*
 * 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 java.util.List;

import org.apache.jmeter.testelement.TestElement;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.oro.text.MalformedCachePatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.Perl5Compiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.auto.service.AutoService;

/**
 * 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.
 * <pre>{@code
 * testf = new LogFilter();
 * String[] incl = { "hello.html", "index.html", "/index.jsp" };
 * String[] theFiles = {
 *     "/test/hello.jsp", "/test/one/hello.html", "hello.jsp", "hello.htm", "/test/open.jsp",
 *     "/test/open.html", "/index.jsp", "/index.jhtml", "newindex.jsp", "oldindex.jsp", "oldindex1.jsp",
 *     "oldindex2.jsp", "oldindex3.jsp", "oldindex4.jsp", "oldindex5.jsp", "oldindex6.jsp", "/test/index.htm"
 * };
 * testf.excludeFiles(incl);
 * System.out.println(" ------------ exclude test -------------");
 * for (String theFile : theFiles) {
 *  boolean fl = testf.isFiltered(theFile);
 *  String line = testf.filter(theFile);
 *  if (line != null) {
 *     System.out.println("the file: " + 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.
 */
@SuppressWarnings("InconsistentCapitalization")
@AutoService(Filter.class)
public class LogFilter implements Filter, Serializable {

    private static final long serialVersionUID = 241L;

    private static final Logger log = LoggerFactory.getLogger(LogFilter.class);

    private static final boolean USE_JAVA_REGEX = !JMeterUtils.getPropDefault(
            "jmeter.regex.engine", "oro").equalsIgnoreCase("oro");

    // 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;

    private final List<String> excludePatternStrings = new ArrayList<>();

    private final List<String> includePatternStrings = new ArrayList<>();

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

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

    protected String NEWFILE = null;

    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.contains(".") && !newext.contains(".")) {
                this.OLDEXT = "." + oldext;
                this.NEWEXT = "." + newext;
            } else {
                this.OLDEXT = oldext;
                this.NEWEXT = newext;
            }
        }
    }

    /**
     * Give the filter a list of files to include
     *
     * @param filenames
     *            list of files to include
     * @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
     *            list of files to exclude
     * @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
     *            list of regular expressions
     * @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 (String includePattern : INCPTRN) {
                this.INCPATTERNS.add(this.createPattern(includePattern));
                this.includePatternStrings.add(includePattern);
            }
        }
    }

    /**
     * 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
     *            list of regular expressions
     *
     * @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 (String excludePattern : EXCPTRN) {
                this.EXCPATTERNS.add(this.createPattern(excludePattern));
                this.excludePatternStrings.add(excludePattern);
            }
        }
    }

    /**
     * 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 exclusion 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 path to be tested
     * @return <code>true</code> if entry should be excluded
     */
    @Override
    public boolean isFiltered(String path,TestElement el) {
        if (this.FILEFILTER) {
            return filterFile(path);
        }
        if (this.PTRNFILTER) {
            return filterPattern(path);
        }
        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 the file to filter
     * @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
     *            name of the file to tested (must not be <code>null</code>)
     * @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 (String includeFile : this.INCFILE) {
            if (text.contains(includeFile)) {
                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
     *            name of the file to be tested (must not be null)
     * @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 (String excludeFile : this.EXCFILE) {
            if (text.contains(excludeFile)) {
                exc = true;
                this.USEFILE = false;
                break;
            }
        }
        return exc;
    }

    /**
     * The current implementation 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 text to be checked
     * @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 text to be checked
     * @return <code>true</code> if text is included
     */
    protected boolean incPattern(String text) {
        if (USE_JAVA_REGEX) {
            return incPatternWithJavaRegex(text);
        }
        return incPatternWithOroRegex(text);
    }

    private boolean incPatternWithJavaRegex(String text) {
        this.USEFILE = false;
        for (String includePattern : this.includePatternStrings) {
            if (JMeterUtils.compilePattern(includePattern).matcher(text).find()) {
                this.USEFILE = true;
                break;
            }
        }
        return this.USEFILE;
    }

    private boolean incPatternWithOroRegex(String text) {
        this.USEFILE = false;
        for (Pattern includePattern : this.INCPATTERNS) {
            if (JMeterUtils.getMatcher().contains(text, includePattern)) {
                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 text to be checked
     * @return <code>true</code> if text is excluded
     */
    protected boolean excPattern(String text) {
        if (USE_JAVA_REGEX) {
            return excPatternWithJavaRegex(text);
        }
        return excPatternWithOroRegex(text);
    }

    private boolean excPatternWithJavaRegex(String text) {
        this.USEFILE = true;
        for (String excludePattern : this.excludePatternStrings) {
            if (JMeterUtils.compilePattern(excludePattern).matcher(text).find()) {
                this.USEFILE = false;
                return true;
            }
        }
        return false;
    }

    private boolean excPatternWithOroRegex(String text) {
        this.USEFILE = true;
        boolean exc = false;
        for (Pattern excludePattern : this.EXCPATTERNS) {
            if (JMeterUtils.getMatcher().contains(text, excludePattern)) {
                exc = true;
                this.USEFILE = false;
                break;
            }
        }
        return exc;
    }

    /**
     * Method uses indexOf to replace the old extension with the new extension.
     * 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
     *            name of the file in which the extension should be replaced
     *            (must not be null)
     * @return <code>true</code> if the extension could be replaced,
     *         <code>false</code> otherwise
     */
    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
     *            string representation of the perl5 compatible regex pattern
     * @return compiled Pattern, or <code>null</code> if no pattern could be
     *         compiled
     */
    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() {

    }
}
