class ExtractorHTMLForms extends Extractor {
    @SuppressWarnings("unused")
    private static final long serialVersionUID = 2L;

    public static final String A_HTML_FORM_OBJECTS = "html-form-objects";

    private static Logger logger =
        Logger.getLogger(ExtractorHTMLForms.class.getName());
 
    {
        setExtractAllForms(false);
    }
    public boolean getExtractAllForms() {
        return (Boolean) kp.get("extractAllForms");
    }
    /**
     * If true, report all FORMs. If false, report only those that
     * appear to be a login-enabling FORM.
     * Default is false.
     */
    public void setExtractAllForms(boolean extractAllForms) {
        kp.put("extractAllForms",extractAllForms);
    }
    
    public ExtractorHTMLForms() {
    }

    protected boolean shouldProcess(CrawlURI uri) {
        return uri.containsDataKey(ExtractorHTML.A_FORM_OFFSETS);
    }

    public void extract(CrawlURI curi) {
        try {
            ReplayCharSequence cs = curi.getRecorder().getContentReplayCharSequence();
            analyze(curi, cs);
        } catch (IOException e) {
            curi.getNonFatalFailures().add(e);
            logger.log(Level.WARNING,"Failed get of replay char sequence in " +
                    Thread.currentThread().getName(), e);
        }
    }

    /**
     * Run analysis: find form METHOD, ACTION, and all INPUT names/values
     * 
     * Log as configured. 
     * 
     * @param curi CrawlURI we're processing.
     * @param cs Sequence from underlying ReplayCharSequence. This
     * is TRANSIENT data. Make a copy if you want the data to live outside
     * of this extractors' lifetime.
     */
    protected void analyze(CrawlURI curi, CharSequence cs) {
        for (Object offset : curi.getDataList(ExtractorHTML.A_FORM_OFFSETS)) {
            int offsetInt = (Integer) offset;
            CharSequence relevantSequence = cs.subSequence(offsetInt, cs.length());
            String method = findAttributeValueGroup("(?i)^[^>]*\\smethod\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,relevantSequence);
            String action = findAttributeValueGroup("(?i)^[^>]*\\saction\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,relevantSequence);
            String enctype = findAttributeValueGroup("(?i)^[^>]*\\senctype\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,relevantSequence);
            HTMLForm form = new HTMLForm();
            form.setMethod(method);
            form.setAction(action);
            form.setEnctype(enctype);
            for(CharSequence input : findGroups("(?i)(<input\\s[^>]*>)|(</?form>)",1,relevantSequence)) {
                String type = findAttributeValueGroup("(?i)^[^>]*\\stype\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,input);
                String name = findAttributeValueGroup("(?i)^[^>]*\\sname\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,input);
                String value = findAttributeValueGroup("(?i)^[^>]*\\svalue\\s*=\\s*([^>\\s]{1,50000})[^>]*>",1,input);
                Matcher m = TextUtils.getMatcher("(?i)^[^>]*\\schecked\\s*[^>]*>", input);
                boolean checked = false;
                try {
                    checked = m.find();
                } finally {
                    TextUtils.recycleMatcher(m);
                }
                form.addField(type, name, value, checked);
            }
            if (form.seemsLoginForm() || getExtractAllForms()) {
                curi.getDataList(A_HTML_FORM_OBJECTS).add(form);
                curi.getAnnotations().add(form.asAnnotation());
            }
        }
    }
    
    protected List<CharSequence> findGroups(String pattern, int groupNumber, CharSequence cs) {
        ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
        Matcher m = TextUtils.getMatcher(pattern, cs);
        try {
            while(m.find()) {
                if(m.group(groupNumber)!=null) {
                    groups.add(cs.subSequence(m.start(groupNumber),m.end(groupNumber)));
                } else {
                    // group not found: end find condition
                    break; 
                }
            } 
            return groups;
        } finally {
            TextUtils.recycleMatcher(m);
        }
    }
    
    protected String findAttributeValueGroup(String pattern, int groupNumber, CharSequence cs) {<FILL_FUNCTION_BODY>}
}

        Matcher m = TextUtils.getMatcher(pattern, cs);
        try {
            if(m.find()) {
                String value = m.group(groupNumber);
                /*
                 * In a case like this <input name="foo"/> the group here will
                 * be "foo"/ ... it's difficult to adjust the regex to avoid
                 * slurping that trailing slash, so handle it here
                 */
                value = StringUtils.removeEnd(value, "'/");
                value = StringUtils.removeEnd(value, "\"/");
                value = StringUtils.strip(value, "\'\""); // strip quotes if present
                return value;
            } else {
                return null; 
            }
        } finally {
            TextUtils.recycleMatcher(m);
        }
    
<methods>public non-sealed void <init>() ,public static void add(org.archive.modules.CrawlURI, int, java.lang.String, org.archive.modules.extractor.LinkContext, org.archive.modules.extractor.Hop) throws URIException,public static org.archive.modules.CrawlURI addRelativeToBase(org.archive.modules.CrawlURI, int, java.lang.CharSequence, org.archive.modules.extractor.LinkContext, org.archive.modules.extractor.Hop) throws URIException,public static org.archive.modules.CrawlURI addRelativeToVia(org.archive.modules.CrawlURI, int, java.lang.String, org.archive.modules.extractor.LinkContext, org.archive.modules.extractor.Hop) throws URIException,public org.archive.modules.extractor.ExtractorParameters getExtractorParameters() ,public org.archive.modules.extractor.UriErrorLoggerModule getLoggerModule() ,public void logUriError(URIException, org.archive.net.UURI, java.lang.CharSequence) ,public java.lang.String report() ,public void setExtractorParameters(org.archive.modules.extractor.ExtractorParameters) ,public void setLoggerModule(org.archive.modules.extractor.UriErrorLoggerModule) <variables>public static final org.archive.modules.extractor.ExtractorParameters DEFAULT_PARAMETERS,private static final java.util.logging.Logger LOGGER,protected transient org.archive.modules.extractor.ExtractorParameters extractorParameters,private static final java.util.logging.Logger logger,protected transient org.archive.modules.extractor.UriErrorLoggerModule loggerModule,protected java.util.concurrent.atomic.AtomicLong numberOfLinksExtracted