com.admc.jcreole.marker.MarkerMap.java Source code

Java tutorial

Introduction

Here is the source code for com.admc.jcreole.marker.MarkerMap.java

Source

/*
 * Copyright 2011 Axis Data Management Corp.
 *
 * Licensed 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 com.admc.jcreole.marker;

import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.TreeMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.admc.jcreole.CreoleParseException;
import com.admc.jcreole.TagType;
import com.admc.jcreole.SectionHeading;
import com.admc.jcreole.Sections;
import com.admc.jcreole.EntryType;

/**
 * @author Blaine Simpson (blaine dot simpson at admc dot com)
 * @since 1.1.1
 */
public class MarkerMap extends HashMap<Integer, BufferMarker> {
    private static Log log = LogFactory.getLog(MarkerMap.class);
    private Sections sections;
    private Map<String, String> idToTextHMap = new HashMap<String, String>();
    private String enumerationFormats;
    private StringBuilder buffer;
    private MasterDefListMarker masterDefListMarker;
    private FootNotesMarker footNotesMarker;
    private IndexMarker indexMarker;

    /**
     * @param enumerationFormats is the starting numerationFormats used for
     *        header elements in the main body (the current body
     *        enumerationFormats can change as HeadingMarkers are encountered).
     *        This setting is independent of TOC levelInclusions, which is
     *        encapsulated nicely in TocMarker instances and not changed here
     *        (or elsewhere).
     */
    public StringBuilder apply(StringBuilder sb, String enumerationFormats) {
        if (enumerationFormats == null)
            throw new NullPointerException("enumerationFormats may not be null");
        buffer = sb;
        for (BufferMarker m : values())
            if (m instanceof MasterDefListMarker) {
                masterDefListMarker = (MasterDefListMarker) m;
            } else if (m instanceof FootNotesMarker) {
                footNotesMarker = (FootNotesMarker) m;
            } else if (m instanceof IndexMarker) {
                indexMarker = (IndexMarker) m;
            }

        this.enumerationFormats = enumerationFormats;
        setContexts();
        List<BufferMarker> sortedMarkers = new ArrayList<BufferMarker>(values());
        Collections.sort(sortedMarkers);
        // Can not run insert() until after the markers have been sorted.
        if (size() < 1)
            return buffer;
        forwardPass1(sortedMarkers);
        int id;
        int offset3 = -1;
        int offset2 = -1;
        int offsetNl;
        EntryType eType;
        String idString;
        String name;
        while ((offset2 = buffer.indexOf("\u0002", offset3 + 1)) > -1) {
            // Load Entries (without data)
            offsetNl = buffer.indexOf("\n", offset2 + 2);
            if (offsetNl < 0)
                throw new CreoleParseException("No name termination for Entry");
            // Unfortunately StringBuilder has no indexOf(char).
            // We could do StringBuilder.toString().indexOf(char), but
            // that's a pretty expensive copy operation.
            offset3 = buffer.indexOf("\u0003", offsetNl + 1);
            if (offset3 < 0)
                throw new CreoleParseException("No termination for Entry");
            name = buffer.substring(offset2 + 2, offsetNl);
            if (name.length() < 1)
                throw new CreoleParseException("Empty embedded name for Entry");
            switch (buffer.charAt(offset2 + 1)) {
            case 'D':
                eType = EntryType.MASTERDEF;
                break;
            case 'F':
                eType = EntryType.FOOTNOTE;
                break;
            default:
                throw new CreoleParseException("Unexpected EntryType indicator: " + buffer.charAt(offset2 + 1));
            }
            if (footNotesMarker != null && eType == EntryType.FOOTNOTE) {
                footNotesMarker.add(name);
            } else if (masterDefListMarker != null && eType == EntryType.MASTERDEF) {
                masterDefListMarker.add(name);
            }
        }

        if (footNotesMarker != null) {
            footNotesMarker.sort();
            footNotesMarker.updateReferences();
        }
        if (masterDefListMarker != null) {
            masterDefListMarker.sort();
            masterDefListMarker.updateReferences();
        }
        if (indexMarker != null) {
            indexMarker.generateEntries();
            indexMarker.sort();
        }

        forwardPass2(sortedMarkers);
        log.debug(Integer.toString(sections.size()) + " Section headings: " + sections);
        // The list of markers MUST BE REVERSE SORTED before applying.
        // Applying in forward order would change buffer offsets.
        Collections.reverse(sortedMarkers);
        for (BufferMarker m : sortedMarkers)
            // N.b. this is where the real APPLY occurs to the buffer:
            if (!(m instanceof BodyUpdaterMarker))
                m.updateBuffer();

        // Can not move Entries until all of the normal \u001a markers have
        // been taken care of, because Styler directives depend on original
        // Creole sequence.

        // Extract all Entries
        offset2 = 0;
        while ((offset2 = buffer.indexOf("\u0002", offset2)) > -1) {
            // Load data for Entries
            offsetNl = buffer.indexOf("\n", offset2 + 2);
            if (offsetNl < 0)
                throw new CreoleParseException("No name termination for Entry");
            // Unfortunately StringBuilder has no indexOf(char).
            // We could do StringBuilder.toString().indexOf(char), but
            // that's a pretty expensive copy operation.
            offset3 = buffer.indexOf("\u0003", offsetNl + 1);
            if (offset3 < 0)
                throw new CreoleParseException("No termination for Entry");
            name = buffer.substring(offset2 + 2, offsetNl);
            if (name.length() < 1)
                throw new CreoleParseException("Empty embedded name for Entry");
            switch (buffer.charAt(offset2 + 1)) {
            case 'D':
                eType = EntryType.MASTERDEF;
                break;
            case 'F':
                eType = EntryType.FOOTNOTE;
                break;
            default:
                throw new CreoleParseException("Unexpected EntryType indicator: " + buffer.charAt(offset2 + 1));
            }
            if (footNotesMarker != null && eType == EntryType.FOOTNOTE)
                footNotesMarker.set(name, buffer.substring(offsetNl + 1, offset3));
            else if (masterDefListMarker != null && eType == EntryType.MASTERDEF)
                masterDefListMarker.set(name, buffer.substring(offsetNl + 1, offset3));
            buffer.delete(offset2, offset3 + 1);
        }

        // TODO: Consider whether to check for \u001a's inside of Entry p's,
        // which must be circular MasterDef or FootNotes markers.
        setContexts();
        Collections.sort(sortedMarkers);
        Collections.reverse(sortedMarkers);
        for (BufferMarker m : sortedMarkers)
            if (m == indexMarker)
                indexMarker.updateBuffer();
            else if (m == footNotesMarker)
                footNotesMarker.updateBuffer();
            else if (m == masterDefListMarker)
                masterDefListMarker.updateBuffer();
        return buffer;
    }

    private Map<String, String> nameToDefHtml = new HashMap<String, String>();

    /**
     * Sets context (buffer and offset) for markers.
     * This must be called before sorting Markers or before calling
     * updateBuffer() on a Marker.
     *
     * @param bodyUpdaterMarkers If true then only update BodyUpdaterMakers,
     *        otherwise then only update non-BodyUpdaterMarkers.
     * @return Number of markers found
     */
    private void setContexts() {
        BufferMarker marker;
        String idString;
        int id;
        int offset = 0;
        while ((offset = buffer.indexOf("\u001a", offset)) > -1)
            try {
                // Unfortunately StringBuilder has no indexOf(char).
                // We could do StringBuilder.toString().indexOf(char), but
                // that's a pretty expensive copy operation.
                if (buffer.length() < offset + 4)
                    throw new CreoleParseException("Marking too close to end of output");
                idString = buffer.substring(offset + 1, offset + 5);
                id = Integer.parseInt(idString, 16);
                marker = get(Integer.valueOf(id));
                if (marker == null)
                    throw new IllegalStateException("Lost marker with id " + id);
                marker.setContext(buffer, offset);
            } finally {
                offset += 5; // Move past the marker that we just found
            }
    }

    /**
     * Marker prep which must occur completely before forwardPass2
     */
    private void forwardPass1(List<BufferMarker> sortedMarkers) {
        HeadingMarker hm;
        SectionHeading sectionHeading;
        Map<String, Integer> nameToRefNum = new HashMap<String, Integer>();
        for (BufferMarker m : sortedMarkers)
            if (m instanceof HeadingMarker) {
                hm = (HeadingMarker) m;
                sectionHeading = hm.getSectionHeading();
                idToTextHMap.put(sectionHeading.getXmlId(), sectionHeading.getText());
            } else if (m instanceof TocMarker) {
                ((TocMarker) m).setDefaultLevelInclusions(enumerationFormats);
            } else if (m instanceof FootNoteRefMarker) {
                if (footNotesMarker != null)
                    footNotesMarker.add((FootNoteRefMarker) m);
            } else if (m instanceof IndexedMarker) {
                if (indexMarker != null)
                    indexMarker.add((IndexedMarker) m);
            } else if (m instanceof DeferredUrlMarker) {
                if (masterDefListMarker != null)
                    masterDefListMarker.add((DeferredUrlMarker) m);
            }
    }

    /**
     * Does lots of stuff during a strictly forward-direction iteration of
     * all Markers .
     * Most of it has to do with resolving styler references.
     * <p>
     * In particular, any automatic behavior dependent upon position within the
     * document must be implemented here.
     * </p>
     */
    private void forwardPass2(List<BufferMarker> sortedMarkers) {
        sections = new Sections();
        final List<TagMarker> stack = new ArrayList<TagMarker>();
        List<? extends TagMarker> typedStack = null;
        final List<String> queuedJcxSpanClassNames = new ArrayList<String>();
        final List<String> queuedJcxBlockClassNames = new ArrayList<String>();
        final List<String> queuedBlockClassNames = new ArrayList<String>();
        final List<String> queuedInlineClassNames = new ArrayList<String>();
        List<String> typedQueue = null;
        String linkText;
        CloseMarker closeM;
        LinkMarker linkM;
        DeferredUrlMarker duM;
        HeadingMarker headingM;
        TagMarker lastTag, tagM;
        final List<JcxSpanMarker> jcxSpanStack = new ArrayList<JcxSpanMarker>();
        final List<JcxBlockMarker> jcxBlockStack = new ArrayList<JcxBlockMarker>();
        final List<BlockMarker> blockStack = new ArrayList<BlockMarker>();
        final List<InlineMarker> inlineStack = new ArrayList<InlineMarker>();
        JcxSpanMarker prevJcxSpan = null;
        JcxBlockMarker prevJcxBlock = null;
        BlockMarker prevBlock = null;
        InlineMarker prevInline = null;
        int headingLevel = 0;
        int[] curSequences = new int[] { -1, -1, -1, -1, -1, -1 };

        for (BufferMarker m : sortedMarkers) {
            if (m instanceof TagMarker) {
                tagM = (TagMarker) m;
                // Get this validation over with so rest of this block can
                // assume tagM is an instance of one of these types.
                if (!(tagM instanceof JcxSpanMarker) && !(tagM instanceof JcxBlockMarker)
                        && !(tagM instanceof BlockMarker) && !(tagM instanceof InlineMarker))
                    throw new RuntimeException(
                            "Unexpected class for TagMarker " + tagM + ": " + tagM.getClass().getName());
                // UPDATE prev/cur
                if (tagM.isAtomic()) {
                    // For atomics we do not deal with stacks, since we would
                    // just push and pop immediately resulting in no change.
                    // Similarly, whatever was cur* before will again be cur*
                    // when we exit this code block.
                    if (tagM instanceof JcxSpanMarker) {
                        prevJcxSpan = (JcxSpanMarker) tagM;
                    } else if (tagM instanceof JcxBlockMarker) {
                        prevJcxBlock = (JcxBlockMarker) tagM;
                    } else if (tagM instanceof BlockMarker) {
                        prevBlock = (BlockMarker) tagM;
                    } else if (tagM instanceof InlineMarker) {
                        prevInline = (InlineMarker) tagM;
                    }
                } else {
                    // Tag has just OPENed.
                    // Opening a tag should not effect prev* tags, since nothing
                    // has closed to become previous.
                    if (tagM instanceof JcxSpanMarker) {
                        jcxSpanStack.add(0, (JcxSpanMarker) tagM);
                    } else if (tagM instanceof JcxBlockMarker) {
                        jcxBlockStack.add(0, (JcxBlockMarker) tagM);
                    } else if (tagM instanceof BlockMarker) {
                        blockStack.add(0, (BlockMarker) tagM);
                    } else if (tagM instanceof InlineMarker) {
                        inlineStack.add(0, (InlineMarker) tagM);
                    }
                    stack.add(0, tagM); // 'lastTag' until another added
                }
                typedQueue = null;
                if (tagM instanceof JcxSpanMarker) {
                    if (queuedJcxSpanClassNames.size() > 0)
                        typedQueue = queuedJcxSpanClassNames;
                } else if (tagM instanceof JcxBlockMarker) {
                    if (queuedJcxBlockClassNames.size() > 0)
                        typedQueue = queuedBlockClassNames;
                } else if (tagM instanceof BlockMarker) {
                    if (queuedBlockClassNames.size() > 0)
                        typedQueue = queuedBlockClassNames;
                } else if (tagM instanceof InlineMarker) {
                    if (queuedInlineClassNames.size() > 0)
                        typedQueue = queuedInlineClassNames;
                }
                if (typedQueue != null) {
                    tagM.addCssClasses(typedQueue.toArray(new String[0]));
                    typedQueue.clear();
                }
            } else if (m instanceof CloseMarker) {
                closeM = (CloseMarker) m;
                lastTag = (stack.size() > 0) ? stack.get(0) : null;
                // Validate tag type
                TagType targetType = closeM.getTargetType();
                try {
                    switch (targetType) {
                    case JCXBLOCK:
                        if (!(lastTag instanceof JcxBlockMarker))
                            throw new Exception();
                        break;
                    case JCXSPAN:
                        if (!(lastTag instanceof JcxSpanMarker))
                            throw new Exception();
                        break;
                    case BLOCK:
                        if (!(lastTag instanceof BlockMarker))
                            throw new Exception();
                        break;
                    case INLINE:
                        if (!(lastTag instanceof InlineMarker))
                            throw new Exception();
                        break;
                    default:
                        throw new IllegalStateException("Unexpected target tag type: " + targetType);
                    }
                } catch (Exception e) {
                    throw new CreoleParseException("Tangled tag nesting.  No matching open " + targetType
                            + " tag for close of " + closeM + ".  Last open tag is " + lastTag + '.', e);
                }
                if (lastTag.isAtomic())
                    throw new CreoleParseException(
                            "Close tag " + closeM + " attempted to close atomic tag " + lastTag + '.');
                // Get this validation over with so rest of this block can
                // assume lastTag is an instance of one of these types.
                if (!(lastTag instanceof JcxSpanMarker) && !(lastTag instanceof JcxBlockMarker)
                        && !(lastTag instanceof BlockMarker) && !(lastTag instanceof InlineMarker))
                    throw new RuntimeException(
                            "Unexpected class for TagMarker " + lastTag + ": " + lastTag.getClass().getName());
                // At this point we have validated match with an opening tag.
                if (lastTag instanceof JcxSpanMarker) {
                    prevJcxSpan = (JcxSpanMarker) lastTag;
                    typedStack = jcxSpanStack;
                } else if (lastTag instanceof JcxBlockMarker) {
                    prevJcxBlock = (JcxBlockMarker) lastTag;
                    typedStack = jcxBlockStack;
                } else if (lastTag instanceof BlockMarker) {
                    prevBlock = (BlockMarker) lastTag;
                    typedStack = blockStack;
                } else if (lastTag instanceof InlineMarker) {
                    prevInline = (InlineMarker) lastTag;
                    typedStack = inlineStack;
                }
                if (typedStack.size() < 1 || typedStack.get(0) != lastTag)
                    throw new CreoleParseException("Closing tag " + lastTag + ", but it is not on the tail of the "
                            + "type-specific tag stack: " + typedStack);
                typedStack.remove(0);
                stack.remove(0);
            } else if (m instanceof Styler) {
                Styler styler = (Styler) m;
                TagType targetType = styler.getTargetType();
                String[] classNames = styler.getClassNames();
                // Get this validation over with so rest of this block can
                // assume targetType is an instance of one of these types.
                switch (targetType) {
                case INLINE:
                case BLOCK:
                case JCXSPAN:
                case JCXBLOCK:
                    break;
                default:
                    throw new RuntimeException("Unexpected tag type value: " + targetType);
                }
                TagMarker targetTag = null;
                switch (styler.getTargetDirection()) {
                case PREVIOUS:
                    switch (targetType) {
                    case INLINE:
                        targetTag = prevInline;
                        break;
                    case BLOCK:
                        targetTag = prevBlock;
                        break;
                    case JCXSPAN:
                        targetTag = prevJcxSpan;
                        break;
                    case JCXBLOCK:
                        targetTag = prevJcxBlock;
                        break;
                    }
                    if (targetTag == null)
                        throw new CreoleParseException("No previous " + targetType + " tag for Styler " + styler);
                    break;
                case CONTAINER:
                    switch (targetType) {
                    case INLINE:
                        typedStack = inlineStack;
                        break;
                    case BLOCK:
                        typedStack = blockStack;
                        break;
                    case JCXSPAN:
                        typedStack = jcxSpanStack;
                        break;
                    case JCXBLOCK:
                        typedStack = jcxBlockStack;
                        break;
                    }
                    if (typedStack.size() < 1)
                        throw new CreoleParseException(
                                "No parent " + targetType + " container for Styler " + styler);
                    targetTag = typedStack.get(0);
                    break;
                case NEXT:
                    switch (targetType) {
                    case INLINE:
                        typedQueue = queuedInlineClassNames;
                        break;
                    case BLOCK:
                        typedQueue = queuedBlockClassNames;
                        break;
                    case JCXSPAN:
                        typedQueue = queuedJcxSpanClassNames;
                        break;
                    case JCXBLOCK:
                        typedQueue = queuedJcxBlockClassNames;
                        break;
                    }
                    typedQueue.addAll(Arrays.asList(classNames));
                    break;
                default:
                    throw new RuntimeException("Unexpected direction value: " + styler.getTargetDirection());
                }
                if (targetTag != null)
                    targetTag.addCssClasses(classNames);
            } else if (m instanceof LinkMarker) {
                linkM = (LinkMarker) m;
                linkText = linkM.getLinkText();
                String lookedUpLabel = idToTextHMap.get(linkM.getLinkText().substring(1));
                if (linkM.getLabel() == null)
                    linkM.setLabel((lookedUpLabel == null) ? linkM.getLinkText() : lookedUpLabel);
                if (lookedUpLabel == null)
                    linkM.wrapLabel("<span class=\"jcreole_orphanLink\">", "</span>");
            } else if (m instanceof TocMarker) {
                ((TocMarker) m).setSectionHeadings(sections);
            } else if (m instanceof BodyUpdaterMarker) {
                ;
            } else if (m instanceof FootNoteRefMarker) {
                ;
            } else if (m instanceof IndexedMarker) {
                ;
            } else if (m instanceof DeferredUrlMarker) {
                ;
            } else {
                throw new CreoleParseException("Unexpected close marker class: " + m.getClass().getName());
            }
            if (m instanceof HeadingMarker) {
                headingM = (HeadingMarker) m;
                SectionHeading sh = headingM.getSectionHeading();
                enumerationFormats = headingM.updatedEnumerationFormats(enumerationFormats);
                sh.setEnumerationFormats(enumerationFormats);
                sections.add(sh);
                int newLevel = sh.getLevel();
                if (newLevel > headingLevel) {
                    headingLevel = newLevel;
                } else if (newLevel < headingLevel) {
                    for (int i = headingLevel; i > newLevel; i--)
                        curSequences[i - 1] = -1;
                    headingLevel = newLevel;
                } else {
                    // No level change
                    // Intentionally empty
                }
                if (headingM.getFormatReset() != null)
                    curSequences[headingLevel - 1] = -1;
                curSequences[headingLevel - 1] += 1;
                sh.setSequences(curSequences);
            }
        }
        if (stack.size() != 0)
            throw new CreoleParseException("Unmatched tag(s) generated: " + stack);
        if (jcxSpanStack.size() != 0)
            throw new CreoleParseException("Unmatched JCX Span tag(s): " + jcxSpanStack);
        if (blockStack.size() != 0)
            throw new CreoleParseException("Unmatched Block tag(s): " + blockStack);
        if (jcxBlockStack.size() != 0)
            throw new CreoleParseException("Unmatched JCX Block tag(s): " + jcxBlockStack);
        if (inlineStack.size() != 0)
            throw new CreoleParseException("Unmatched Inline tag(s): " + inlineStack);
        if (queuedJcxSpanClassNames.size() > 0)
            throw new CreoleParseException("Unapplied Styler JCX class names: " + queuedJcxSpanClassNames);
        if (queuedJcxBlockClassNames.size() > 0)
            throw new CreoleParseException("Unapplied Styler JCX Block class names: " + queuedBlockClassNames);
        if (queuedBlockClassNames.size() > 0)
            throw new CreoleParseException("Unapplied Styler Block class names: " + queuedBlockClassNames);
        if (queuedInlineClassNames.size() > 0)
            throw new CreoleParseException("Unapplied Styler Inline class names: " + queuedInlineClassNames);
    }

    public Sections getSectionHeadings() {
        return sections;
    }
}