class FetchHistoryProcessor extends Processor {
    @SuppressWarnings("unused")
    private static final long serialVersionUID = 1L;
    
    /** Desired history array length. */
    protected int historyLength = 2;
    public int getHistoryLength() {
        return this.historyLength;
    }
    public void setHistoryLength(int length) {
        this.historyLength = length;
    }
//    key description: "Number of previous fetch entries to retain in the URI " +
//    "history. The current fetch becomes a history entry at " +
//    "this Processor step, so the smallest useful value is " +
//    "'2' (including the current fetch). Default is '2'."
    
    // class description: "FetchHistoryProcessor. Maintain a history of fetch " +
    // "information inside the CrawlURI's attributes.."
    
    public FetchHistoryProcessor() {
    }

    @Override
    protected void innerProcess(CrawlURI puri) throws InterruptedException {
        CrawlURI curi = (CrawlURI) puri;
        HashMap<String, Object> latestFetch = new HashMap<String, Object>();

        // save status
        latestFetch.put(A_STATUS, curi.getFetchStatus());
        // save fetch start time
        latestFetch.put(A_FETCH_BEGAN_TIME, curi.getFetchBeginTime());
        // save digest
        String digest = curi.getContentDigestSchemeString();
        if (digest != null) {
            latestFetch.put(A_CONTENT_DIGEST, digest);
        }
        // save relevant HTTP headers, if available
        if (curi.isHttpTransaction()) {
            saveHeader(curi, latestFetch, A_ETAG_HEADER);
            saveHeader(curi, latestFetch, A_LAST_MODIFIED_HEADER);

            // save reference length (real or virtual)
            long referenceLength;
            if (curi.containsDataKey(A_REFERENCE_LENGTH)) {
                // reuse previous length if available (see FetchHTTP#setSizes).
                referenceLength = (Long) curi.getData().get(A_REFERENCE_LENGTH);
            } else {
                // normally, use content-length
                referenceLength = curi.getContentLength();
            }
            latestFetch.put(A_REFERENCE_LENGTH, referenceLength);
        }

        HashMap<String, Object>[] history = historyRealloc(curi);

        // rotate all history entries up one slot, insert new at [0]
        for (int i = history.length - 1; i > 0; i--) {
            history[i] = history[i - 1];
        }
        history[0] = latestFetch;

        curi.setFetchHistory(history);

        if (curi.getFetchStatus() == 304) {
            if( history.length >= 2 && history[1] != null && history[1].containsKey(A_CONTENT_DIGEST)) {
                // Copy forward the content digest as the current digest is simply of an empty response
                latestFetch.put(A_CONTENT_DIGEST, history[1].get(A_CONTENT_DIGEST));
                // Create revisit profile
                curi.getAnnotations().add("duplicate:server-not-modified");
                ServerNotModifiedRevisit revisit = new ServerNotModifiedRevisit();
                revisit.setETag((String) latestFetch.get(A_ETAG_HEADER));
                revisit.setLastModified((String) latestFetch.get(A_LAST_MODIFIED_HEADER));
                revisit.setPayloadDigest((String)latestFetch.get(A_CONTENT_DIGEST));
                curi.setRevisitProfile(revisit);
            }
        } else if (hasIdenticalDigest(curi)) {
            curi.getAnnotations().add("duplicate:digest");
            IdenticalPayloadDigestRevisit revisit = 
            		new IdenticalPayloadDigestRevisit((String)history[1].get(A_CONTENT_DIGEST));
            revisit.setRefersToTargetURI(curi.getURI()); // Matches are always on the same URI
            revisit.setRefersToDate((Long)history[1].get(A_FETCH_BEGAN_TIME));
            curi.setRevisitProfile(revisit);
        }
    }

    /**
     * Utility method for testing if a CrawlURI's last two history 
     * entries (one being the most recent fetch) have identical 
     * content-digest information. 
     * 
     * @param curi CrawlURI to test
     * @return true if last two history entries have identical digests, 
     * otherwise false
     */
    public static boolean hasIdenticalDigest(CrawlURI curi) {
        Map<String,Object>[] history = curi.getFetchHistory();

        return history != null
                && history[0] != null 
                && history[0].containsKey(A_CONTENT_DIGEST)
                && history[1] != null
                && history[1].containsKey(A_CONTENT_DIGEST)
                && history[0].get(A_CONTENT_DIGEST).equals(history[1].get(A_CONTENT_DIGEST));
    }
    
    /** Get or create proper-sized history array */
    @SuppressWarnings("unchecked")
    protected HashMap<String, Object>[] historyRealloc(CrawlURI curi) {<FILL_FUNCTION_BODY>}

    /** Save a header from the given HTTP operation into the Map. */
    protected void saveHeader(CrawlURI curi, Map<String,Object> map,
            String key) {
        String value = curi.getHttpResponseHeader(key);
        if (value != null) {
            map.put(key, value);
        }
    }

    @Override
    protected boolean shouldProcess(CrawlURI curi) {
        // only process if curi contains evidence of fetch attempt
        return curi.containsDataKey(A_FETCH_BEGAN_TIME);
    }
}

        int targetHistoryLength = getHistoryLength();
        HashMap<String, Object>[] history = curi.getFetchHistory();
        if (history == null) {
            history = new HashMap[targetHistoryLength];
        }
        if (history.length != targetHistoryLength) {
            HashMap<String, Object>[] newHistory = new HashMap[targetHistoryLength];
            System.arraycopy(history, 0, newHistory, 0,
                    Math.min(history.length, newHistory.length));
            history = newHistory;
        }
        return history;
    
<methods>public non-sealed void <init>() ,public void doCheckpoint(org.archive.checkpointing.Checkpoint) throws java.io.IOException,public void finishCheckpoint(org.archive.checkpointing.Checkpoint) ,public static java.lang.String flattenVia(org.archive.modules.CrawlURI) ,public java.lang.String getBeanName() ,public boolean getEnabled() ,public org.archive.spring.KeyedProperties getKeyedProperties() ,public static long getRecordedSize(org.archive.modules.CrawlURI) ,public org.archive.modules.deciderules.DecideRule getShouldProcessRule() ,public long getURICount() ,public static boolean hasHttpAuthenticationCredential(org.archive.modules.CrawlURI) ,public boolean isRunning() ,public static boolean isSuccess(org.archive.modules.CrawlURI) ,public org.archive.modules.ProcessResult process(org.archive.modules.CrawlURI) throws java.lang.InterruptedException,public java.lang.String report() ,public void setBeanName(java.lang.String) ,public void setEnabled(boolean) ,public void setRecoveryCheckpoint(org.archive.checkpointing.Checkpoint) ,public void setShouldProcessRule(org.archive.modules.deciderules.DecideRule) ,public void start() ,public void startCheckpoint(org.archive.checkpointing.Checkpoint) ,public void stop() <variables>protected java.lang.String beanName,protected boolean isRunning,protected org.archive.spring.KeyedProperties kp,protected org.archive.checkpointing.Checkpoint recoveryCheckpoint,protected java.util.concurrent.atomic.AtomicLong uriCount