at.bestsolution.fxide.jdt.text.javadoc.JavadocContentAccess2.java Source code

Java tutorial

Introduction

Here is the source code for at.bestsolution.fxide.jdt.text.javadoc.JavadocContentAccess2.java

Source

/*******************************************************************************
 * Copyright (c) 2008, 2015 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Tom Hofmann, Google <eclipse@tom.eicher.name> - [hovering] NPE when hovering over @value reference within a type's javadoc - https://bugs.eclipse.org/bugs/show_bug.cgi?id=320084
 *******************************************************************************/
package at.bestsolution.fxide.jdt.text.javadoc;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IClasspathAttribute;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJarEntryResource;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ILocalVariable;
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageDeclaration;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.ITypeParameter;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MemberRef;
import org.eclipse.jdt.core.dom.MethodRef;
import org.eclipse.jdt.core.dom.MethodRefParameter;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.TextElement;

import at.bestsolution.fxide.jdt.corext.dom.ASTNodes;
import at.bestsolution.fxide.jdt.corext.dom.ASTProvider;
import at.bestsolution.fxide.jdt.corext.javadoc.JavaDocLocations;
import at.bestsolution.fxide.jdt.corext.util.JavaModelUtil;
import at.bestsolution.fxide.jdt.corext.util.JdtFlags;
import at.bestsolution.fxide.jdt.corext.util.Messages;
import at.bestsolution.fxide.jdt.corext.util.MethodOverrideTester;
import at.bestsolution.fxide.jdt.corext.util.SuperTypeHierarchyCache;
import at.bestsolution.fxide.jdt.text.JavaElementLabels;
import at.bestsolution.fxide.jdt.text.viewersupport.JavaElementLinks;

/**
 * Helper to get the content of a Javadoc comment as HTML.
 *
 * <p>
 * <strong>This is work in progress. Parts of this will later become
 * API through {@link JavadocContentAccess}</strong>
 * </p>
 *
 * @since 3.4
 */
public class JavadocContentAccess2 {

    private static final String BASE_URL_COMMENT_INTRO = "<!-- baseURL=\""; //$NON-NLS-1$

    private static final String BLOCK_TAG_START = "<dl>"; //$NON-NLS-1$
    private static final String BLOCK_TAG_END = "</dl>"; //$NON-NLS-1$

    private static final String BlOCK_TAG_ENTRY_START = "<dd>"; //$NON-NLS-1$
    private static final String BlOCK_TAG_ENTRY_END = "</dd>"; //$NON-NLS-1$

    private static final String PARAM_NAME_START = "<b>"; //$NON-NLS-1$
    private static final String PARAM_NAME_END = "</b> "; //$NON-NLS-1$

    /**
     * Implements the "Algorithm for Inheriting Method Comments" as specified for <a href=
     * "http://download.oracle.com/javase/1.4.2/docs/tooldocs/solaris/javadoc.html#inheritingcomments"
     * >1.4.2</a>, <a href=
     * "http://download.oracle.com/javase/1.5.0/docs/tooldocs/windows/javadoc.html#inheritingcomments"
     * >1.5</a>, and <a href=
     * "http://download.oracle.com/javase/6/docs/technotes/tools/windows/javadoc.html#inheritingcomments"
     * >1.6</a>.
     *
     * <p>
     * Unfortunately, the implementation is broken in Javadoc implementations since 1.5, see <a
     * href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6376959">Sun's bug</a>.
     * </p>
     *
     * <p>
     * We adhere to the spec.
     * </p>
     */
    private static abstract class InheritDocVisitor {
        public static final Object STOP_BRANCH = new Object() {
            @Override
            public String toString() {
                return "STOP_BRANCH"; //$NON-NLS-1$
            }
        };
        public static final Object CONTINUE = new Object() {
            @Override
            public String toString() {
                return "CONTINUE"; //$NON-NLS-1$
            }
        };

        /**
         * Visits a type and decides how the visitor should proceed.
         *
         * @param currType the current type
         * @return <ul>
         *         <li>{@link #STOP_BRANCH} to indicate that no Javadoc has been found and visiting
         *         super types should stop here</li>
         *         <li>{@link #CONTINUE} to indicate that no Javadoc has been found and visiting
         *         super types should continue</li>
         *         <li>an {@link Object} or <code>null</code>, to indicate that visiting should be
         *         cancelled immediately. The returned value is the result of
         *         {@link #visitInheritDoc(IType, ITypeHierarchy)}</li>
         *         </ul>
         * @throws JavaModelException unexpected problem
         * @see #visitInheritDoc(IType, ITypeHierarchy)
         */
        public abstract Object visit(IType currType) throws JavaModelException;

        /**
         * Visits the super types of the given <code>currentType</code>.
         *
         * @param currentType the starting type
         * @param typeHierarchy a super type hierarchy that contains <code>currentType</code>
         * @return the result from a call to {@link #visit(IType)}, or <code>null</code> if none of
         *         the calls returned a result
         * @throws JavaModelException unexpected problem
         */
        public Object visitInheritDoc(IType currentType, ITypeHierarchy typeHierarchy) throws JavaModelException {
            ArrayList<IType> visited = new ArrayList<>();
            visited.add(currentType);
            Object result = visitInheritDocInterfaces(visited, currentType, typeHierarchy);
            if (result != InheritDocVisitor.CONTINUE)
                return result;

            IType superClass;
            if (currentType.isInterface())
                superClass = currentType.getJavaProject().findType("java.lang.Object"); //$NON-NLS-1$
            else
                superClass = typeHierarchy.getSuperclass(currentType);

            while (superClass != null && !visited.contains(superClass)) {
                result = visit(superClass);
                if (result == InheritDocVisitor.STOP_BRANCH) {
                    return null;
                } else if (result == InheritDocVisitor.CONTINUE) {
                    visited.add(superClass);
                    result = visitInheritDocInterfaces(visited, superClass, typeHierarchy);
                    if (result != InheritDocVisitor.CONTINUE)
                        return result;
                    else
                        superClass = typeHierarchy.getSuperclass(superClass);
                } else {
                    return result;
                }
            }

            return null;
        }

        /**
         * Visits the super interfaces of the given type in the given hierarchy, thereby skipping already visited types.
         *
         * @param visited set of visited types
         * @param currentType type whose super interfaces should be visited
         * @param typeHierarchy type hierarchy (must include <code>currentType</code>)
         * @return the result, or {@link #CONTINUE} if no result has been found
         * @throws JavaModelException unexpected problem
         */
        private Object visitInheritDocInterfaces(ArrayList<IType> visited, IType currentType,
                ITypeHierarchy typeHierarchy) throws JavaModelException {
            ArrayList<IType> toVisitChildren = new ArrayList<>();
            IType[] superInterfaces = typeHierarchy.getSuperInterfaces(currentType);
            for (int i = 0; i < superInterfaces.length; i++) {
                IType superInterface = superInterfaces[i];
                if (visited.contains(superInterface))
                    continue;
                visited.add(superInterface);
                Object result = visit(superInterface);
                if (result == InheritDocVisitor.STOP_BRANCH) {
                    //skip
                } else if (result == InheritDocVisitor.CONTINUE) {
                    toVisitChildren.add(superInterface);
                } else {
                    return result;
                }
            }
            for (Iterator<IType> iter = toVisitChildren.iterator(); iter.hasNext();) {
                IType child = iter.next();
                Object result = visitInheritDocInterfaces(visited, child, typeHierarchy);
                if (result != InheritDocVisitor.CONTINUE)
                    return result;
            }
            return InheritDocVisitor.CONTINUE;
        }
    }

    private static class JavadocLookup {
        private static final JavadocLookup NONE = new JavadocLookup(null) {
            @Override
            public CharSequence getInheritedMainDescription(IMethod method) {
                return null;
            }

            @Override
            public CharSequence getInheritedParamDescription(IMethod method, int i) {
                return null;
            }

            @Override
            public CharSequence getInheritedReturnDescription(IMethod method) {
                return null;
            }

            @Override
            public CharSequence getInheritedExceptionDescription(IMethod method, String name) {
                return null;
            }
        };

        private static interface DescriptionGetter {
            /**
             * Returns a Javadoc tag description or <code>null</code>.
             *
             * @param contentAccess the content access
             * @return the description, or <code>null</code> if none
             * @throws JavaModelException unexpected problem
             */
            CharSequence getDescription(JavadocContentAccess2 contentAccess) throws JavaModelException;
        }

        private final IType fStartingType;
        private final HashMap<IMethod, JavadocContentAccess2> fContentAccesses;

        private ITypeHierarchy fTypeHierarchy;
        private MethodOverrideTester fOverrideTester;

        private JavadocLookup(IType startingType) {
            fStartingType = startingType;
            fContentAccesses = new HashMap<>();
        }

        /**
         * For the given method, returns the main description from an overridden method.
         *
         * @param method a method
         * @return the description that replaces the <code>{&#64;inheritDoc}</code> tag,
         *       or <code>null</code> if none could be found
         */
        public CharSequence getInheritedMainDescription(IMethod method) {
            return getInheritedDescription(method, new DescriptionGetter() {
                @Override
                public CharSequence getDescription(JavadocContentAccess2 contentAccess) {
                    return contentAccess.getMainDescription();
                }
            });
        }

        /**
         * For the given method, returns the @param tag description for the given type parameter
         * from an overridden method.
         *
         * @param method a method
         * @param typeParamIndex the index of the type parameter
         * @return the description that replaces the <code>{&#64;inheritDoc}</code> tag, or
         *         <code>null</code> if none could be found
         */
        public CharSequence getInheritedTypeParamDescription(IMethod method, final int typeParamIndex) {
            return getInheritedDescription(method, new DescriptionGetter() {
                @Override
                public CharSequence getDescription(JavadocContentAccess2 contentAccess) throws JavaModelException {
                    return contentAccess.getInheritedTypeParamDescription(typeParamIndex);
                }
            });
        }

        /**
         * For the given method, returns the @param tag description for the given parameter from an
         * overridden method.
         *
         * @param method a method
         * @param paramIndex the index of the parameter
         * @return the description that replaces the <code>{&#64;inheritDoc}</code> tag,
         *       or <code>null</code> if none could be found
         */
        public CharSequence getInheritedParamDescription(IMethod method, final int paramIndex) {
            return getInheritedDescription(method, new DescriptionGetter() {
                @Override
                public CharSequence getDescription(JavadocContentAccess2 contentAccess) throws JavaModelException {
                    return contentAccess.getInheritedParamDescription(paramIndex);
                }
            });
        }

        /**
         * For the given method, returns the @return tag description from an overridden method.
         *
         * @param method a method
         * @return the description that replaces the <code>{&#64;inheritDoc}</code> tag,
         *       or <code>null</code> if none could be found
         */
        public CharSequence getInheritedReturnDescription(IMethod method) {
            return getInheritedDescription(method, new DescriptionGetter() {
                @Override
                public CharSequence getDescription(JavadocContentAccess2 contentAccess) {
                    return contentAccess.getReturnDescription();
                }
            });
        }

        /**
         * For the given method, returns the @throws/@exception tag description for the given
         * exception from an overridden method.
         *
         * @param method a method
         * @param simpleName the simple name of an exception
         * @return the description that replaces the <code>{&#64;inheritDoc}</code> tag,
         *       or <code>null</code> if none could be found
         */
        public CharSequence getInheritedExceptionDescription(IMethod method, final String simpleName) {
            return getInheritedDescription(method, new DescriptionGetter() {
                @Override
                public CharSequence getDescription(JavadocContentAccess2 contentAccess) {
                    return contentAccess.getExceptionDescription(simpleName);
                }
            });
        }

        private CharSequence getInheritedDescription(final IMethod method,
                final DescriptionGetter descriptionGetter) {
            try {
                return (CharSequence) new InheritDocVisitor() {
                    @Override
                    public Object visit(IType currType) throws JavaModelException {
                        IMethod overridden = getOverrideTester().findOverriddenMethodInType(currType, method);
                        if (overridden == null)
                            return InheritDocVisitor.CONTINUE;

                        JavadocContentAccess2 contentAccess = getJavadocContentAccess(overridden);
                        if (contentAccess == null) {
                            if (overridden.getOpenable().getBuffer() == null) {
                                // Don't continue this branch when no source is available.
                                // We don't extract individual tags from Javadoc attachments,
                                // and it would be wrong to copy doc from further up the branch,
                                // thereby skipping doc from this overridden method.
                                return InheritDocVisitor.STOP_BRANCH;
                            } else {
                                return InheritDocVisitor.CONTINUE;
                            }
                        }

                        CharSequence overriddenDescription = descriptionGetter.getDescription(contentAccess);
                        if (overriddenDescription != null)
                            return overriddenDescription;
                        else
                            return InheritDocVisitor.CONTINUE;
                    }
                }.visitInheritDoc(method.getDeclaringType(), getTypeHierarchy());
            } catch (JavaModelException e) {
                // TODO
                e.printStackTrace();
            }
            return null;
        }

        /**
         * @param method the method
         * @return the Javadoc content access for the given method, or
         *       <code>null</code> if no Javadoc could be found in source
         * @throws JavaModelException unexpected problem
         */
        private JavadocContentAccess2 getJavadocContentAccess(IMethod method) throws JavaModelException {
            Object cached = fContentAccesses.get(method);
            if (cached != null)
                return (JavadocContentAccess2) cached;
            if (fContentAccesses.containsKey(method))
                return null;

            IBuffer buf = method.getOpenable().getBuffer();
            if (buf == null) { // no source attachment found
                fContentAccesses.put(method, null);
                return null;
            }

            ISourceRange javadocRange = method.getJavadocRange();
            if (javadocRange == null) {
                fContentAccesses.put(method, null);
                return null;
            }

            String rawJavadoc = buf.getText(javadocRange.getOffset(), javadocRange.getLength());
            Javadoc javadoc = getJavadocNode(method, rawJavadoc);
            if (javadoc == null) {
                fContentAccesses.put(method, null);
                return null;
            }

            JavadocContentAccess2 contentAccess = new JavadocContentAccess2(method, javadoc, rawJavadoc, this);
            fContentAccesses.put(method, contentAccess);
            return contentAccess;
        }

        private ITypeHierarchy getTypeHierarchy() throws JavaModelException {
            if (fTypeHierarchy == null)
                fTypeHierarchy = SuperTypeHierarchyCache.getTypeHierarchy(fStartingType);
            return fTypeHierarchy;
        }

        private MethodOverrideTester getOverrideTester() throws JavaModelException {
            if (fOverrideTester == null)
                fOverrideTester = SuperTypeHierarchyCache.getMethodOverrideTester(fStartingType);
            return fOverrideTester;
        }
    }

    /**
     * Either an IMember or an IPackageFragment.
     */
    private final IJavaElement fElement;

    /**
     * The method, or <code>null</code> if {@link #fElement} is not a method where @inheritDoc could
     * work.
     */
    private final IMethod fMethod;
    private final Javadoc fJavadoc;
    private final String fSource;
    private final JavadocLookup fJavadocLookup;

    private StringBuffer fBuf;
    private int fLiteralContent;
    private StringBuffer fMainDescription;
    private StringBuffer fReturnDescription;
    private StringBuffer[] fTypeParamDescriptions;
    private StringBuffer[] fParamDescriptions;
    private HashMap<String, StringBuffer> fExceptionDescriptions;

    private JavadocContentAccess2(IJavaElement element, Javadoc javadoc, String source, JavadocLookup lookup) {
        Assert.isNotNull(element);
        Assert.isTrue(element instanceof IMethod || element instanceof ILocalVariable
                || element instanceof ITypeParameter);
        fElement = element;
        fMethod = (IMethod) ((element instanceof ILocalVariable || element instanceof ITypeParameter)
                ? element.getParent()
                : element);
        fJavadoc = javadoc;
        fSource = source;
        fJavadocLookup = lookup;
    }

    private JavadocContentAccess2(IJavaElement element, Javadoc javadoc, String source) {
        Assert.isTrue(element instanceof IMember || element instanceof IPackageFragment
                || element instanceof ILocalVariable || element instanceof ITypeParameter);
        fElement = element;
        fMethod = null;
        fJavadoc = javadoc;
        fSource = source;
        fJavadocLookup = JavadocLookup.NONE;
    }

    /**
     * Gets an IJavaElement's Javadoc comment content from the source or Javadoc attachment
     * and renders the tags and links in HTML.
     * Returns <code>null</code> if the element does not have a Javadoc comment or if no source is available.
     *
     * @param element            the element to get the Javadoc of
     * @param useAttachedJavadoc   if <code>true</code> Javadoc will be extracted from attached Javadoc
     *                            if there's no source
     * @return the Javadoc comment content in HTML or <code>null</code> if the element
     *          does not have a Javadoc comment or if no source is available
     * @throws CoreException is thrown when the element's Javadoc cannot be accessed
     */
    public static String getHTMLContent(IJavaElement element, boolean useAttachedJavadoc) throws CoreException {
        if (element instanceof IPackageFragment) {
            return getHTMLContent((IPackageFragment) element);
        }
        if (element instanceof IPackageDeclaration) {
            return getHTMLContent((IPackageDeclaration) element);
        }
        if (!(element instanceof IMember || element instanceof ITypeParameter
                || (element instanceof ILocalVariable && (((ILocalVariable) element).isParameter())))) {
            return null;
        }
        String sourceJavadoc = getHTMLContentFromSource(element);
        if (sourceJavadoc == null || sourceJavadoc.length() == 0 || sourceJavadoc.trim().equals("{@inheritDoc}")) { //$NON-NLS-1$
            if (useAttachedJavadoc) {
                if (element.getOpenable().getBuffer() == null) { // only if no source available
                    return element.getAttachedJavadoc(null);
                }
                IMember member = null;
                if (element instanceof ILocalVariable) {
                    member = ((ILocalVariable) element).getDeclaringMember();
                } else if (element instanceof ITypeParameter) {
                    member = ((ITypeParameter) element).getDeclaringMember();
                } else if (element instanceof IMember) {
                    member = (IMember) element;
                }
                if (canInheritJavadoc(member)) {
                    IMethod method = (IMethod) member;
                    String attachedDocInHierarchy = findAttachedDocInHierarchy(method);

                    // Prepend "Overrides:" / "Specified by:" reference headers to make clear
                    // that description has been copied from super method.
                    if (attachedDocInHierarchy == null)
                        return sourceJavadoc;
                    StringBuffer superMethodReferences = createSuperMethodReferences(method);
                    if (superMethodReferences == null)
                        return attachedDocInHierarchy;
                    superMethodReferences.append(attachedDocInHierarchy);
                    return superMethodReferences.toString();
                }
            }
        }
        return sourceJavadoc;
    }

    private static StringBuffer createSuperMethodReferences(final IMethod method) throws JavaModelException {
        IType type = method.getDeclaringType();
        ITypeHierarchy hierarchy = SuperTypeHierarchyCache.getTypeHierarchy(type);
        final MethodOverrideTester tester = SuperTypeHierarchyCache.getMethodOverrideTester(type);

        final ArrayList<IMethod> superInterfaceMethods = new ArrayList<>();
        final IMethod[] superClassMethod = { null };
        new InheritDocVisitor() {
            @Override
            public Object visit(IType currType) throws JavaModelException {
                IMethod overridden = tester.findOverriddenMethodInType(currType, method);
                if (overridden == null)
                    return InheritDocVisitor.CONTINUE;

                if (currType.isInterface())
                    superInterfaceMethods.add(overridden);
                else
                    superClassMethod[0] = overridden;

                return STOP_BRANCH;
            }
        }.visitInheritDoc(type, hierarchy);

        boolean hasSuperInterfaceMethods = superInterfaceMethods.size() != 0;
        if (!hasSuperInterfaceMethods && superClassMethod[0] == null)
            return null;

        StringBuffer buf = new StringBuffer();
        buf.append("<div>"); //$NON-NLS-1$
        if (hasSuperInterfaceMethods) {
            buf.append("<b>"); //$NON-NLS-1$
            buf.append(JavaDocMessages.JavaDoc2HTMLTextReader_specified_by_section);
            buf.append("</b> "); //$NON-NLS-1$
            for (Iterator<IMethod> iter = superInterfaceMethods.iterator(); iter.hasNext();) {
                IMethod overridden = iter.next();
                buf.append(createMethodInTypeLinks(overridden));
                if (iter.hasNext())
                    buf.append(JavaElementLabels.COMMA_STRING);
            }
        }
        if (superClassMethod[0] != null) {
            if (hasSuperInterfaceMethods)
                buf.append(JavaElementLabels.COMMA_STRING);
            buf.append("<b>"); //$NON-NLS-1$
            buf.append(JavaDocMessages.JavaDoc2HTMLTextReader_overrides_section);
            buf.append("</b> "); //$NON-NLS-1$
            buf.append(createMethodInTypeLinks(superClassMethod[0]));
        }
        buf.append("</div>"); //$NON-NLS-1$
        return buf;
    }

    private static String createMethodInTypeLinks(IMethod overridden) {
        CharSequence methodLink = createSimpleMemberLink(overridden);
        CharSequence typeLink = createSimpleMemberLink(overridden.getDeclaringType());
        String methodInType = MessageFormat.format(JavaDocMessages.JavaDoc2HTMLTextReader_method_in_type,
                new Object[] { methodLink, typeLink });
        return methodInType;
    }

    private static CharSequence createSimpleMemberLink(IMember member) {
        StringBuffer buf = new StringBuffer();
        buf.append("<a href='"); //$NON-NLS-1$
        try {
            String uri = JavaElementLinks.createURI(JavaElementLinks.JAVADOC_SCHEME, member);
            buf.append(uri);
        } catch (URISyntaxException e) {
            // TODO
            e.printStackTrace();
        }
        buf.append("'>"); //$NON-NLS-1$
        JavaElementLabels.getElementLabel(member, 0, buf);
        buf.append("</a>"); //$NON-NLS-1$
        return buf;
    }

    private static String getHTMLContentFromSource(IJavaElement element) throws JavaModelException {
        IMember member;
        if (element instanceof ILocalVariable) {
            member = ((ILocalVariable) element).getDeclaringMember();
        } else if (element instanceof ITypeParameter) {
            member = ((ITypeParameter) element).getDeclaringMember();
        } else if (element instanceof IMember) {
            member = (IMember) element;
        } else {
            return null;
        }

        IBuffer buf = member.getOpenable().getBuffer();
        if (buf == null) {
            return null; // no source attachment found
        }

        ISourceRange javadocRange = member.getJavadocRange();
        if (javadocRange == null) {
            if (canInheritJavadoc(member)) {
                // Try to use the inheritDoc algorithm.
                String inheritedJavadoc = javadoc2HTML(member, element, "/***/"); //$NON-NLS-1$
                if (inheritedJavadoc != null && inheritedJavadoc.length() > 0) {
                    return inheritedJavadoc;
                }
            }
            return getJavaFxPropertyDoc(member);
        }

        String rawJavadoc = buf.getText(javadocRange.getOffset(), javadocRange.getLength());
        return javadoc2HTML(member, element, rawJavadoc);
    }

    private static String getJavaFxPropertyDoc(IMember member) throws JavaModelException {
        // XXX: should not do this by default (but we don't have settings for Javadoc, see https://bugs.eclipse.org/424283 )
        if (member instanceof IMethod) {
            String name = member.getElementName();
            boolean isGetter = name.startsWith("get") && name.length() > 3; //$NON-NLS-1$
            boolean isBooleanGetter = name.startsWith("is") && name.length() > 2; //$NON-NLS-1$
            boolean isSetter = name.startsWith("set") && name.length() > 3; //$NON-NLS-1$

            if (isGetter || isBooleanGetter || isSetter) {
                String propertyName = firstToLower(name.substring(isBooleanGetter ? 2 : 3));
                IType type = member.getDeclaringType();
                IMethod method = type.getMethod(propertyName + "Property", new String[0]); //$NON-NLS-1$

                if (method.exists()) {
                    String content = getHTMLContentFromSource(method);
                    if (content != null) {
                        if (isSetter) {
                            content = Messages.format(JavaDocMessages.JavadocContentAccess2_setproperty_message,
                                    new Object[] { propertyName, content });
                        } else {
                            content = Messages.format(JavaDocMessages.JavadocContentAccess2_getproperty_message,
                                    new Object[] { propertyName, content });
                        }
                    }
                    return content;
                }
            } else if (name.endsWith("Property")) { //$NON-NLS-1$
                String propertyName = name.substring(0, name.length() - 8);

                IType type = member.getDeclaringType();
                IField field = type.getField(propertyName);
                if (field.exists()) {
                    return getHTMLContentFromSource(field);
                }
            }
        }
        return null;
    }

    private static String firstToLower(String propertyName) {
        char[] c = propertyName.toCharArray();
        c[0] = Character.toLowerCase(c[0]);
        return String.valueOf(c);
    }

    private static Javadoc getJavadocNode(IJavaElement element, String rawJavadoc) {
        //FIXME: take from SharedASTProvider if available
        //Caveat: Javadoc nodes are not available when Javadoc processing has been disabled!
        //https://bugs.eclipse.org/bugs/show_bug.cgi?id=212207

        String source = rawJavadoc + "class C{}"; //$NON-NLS-1$
        CompilationUnit root = createAST(element, source);
        if (root == null)
            return null;
        List<AbstractTypeDeclaration> types = root.types();
        if (types.size() != 1)
            return null;
        AbstractTypeDeclaration type = types.get(0);
        return type.getJavadoc();
    }

    private static Javadoc getPackageJavadocNode(IJavaElement element, String cuSource) {
        CompilationUnit cu = createAST(element, cuSource);
        if (cu != null) {
            PackageDeclaration packDecl = cu.getPackage();
            if (packDecl != null) {
                return packDecl.getJavadoc();
            }
        }
        return null;
    }

    private static CompilationUnit createAST(IJavaElement element, String cuSource) {
        Assert.isNotNull(element);
        ASTParser parser = ASTParser.newParser(ASTProvider.SHARED_AST_LEVEL);

        IJavaProject javaProject = element.getJavaProject();
        parser.setProject(javaProject);
        Map<String, String> options = javaProject.getOptions(true);
        options.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, JavaCore.ENABLED); // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=212207
        parser.setCompilerOptions(options);

        parser.setSource(cuSource.toCharArray());
        return (CompilationUnit) parser.createAST(null);
    }

    private static String javadoc2HTML(IMember member, IJavaElement element, String rawJavadoc) {
        rawJavadoc = CodeSanatizer.sanatizeCodeContent(rawJavadoc);
        Javadoc javadoc = getJavadocNode(member, rawJavadoc);

        if (javadoc == null) {
            Reader contentReader = null;
            // fall back to JavadocContentAccess:
            try {
                contentReader = JavadocContentAccess.getHTMLContentReader(member, false, false);
                if (contentReader != null)
                    return getString(contentReader);
            } catch (JavaModelException e) {
                // TODO
                e.printStackTrace();
            } finally {
                if (contentReader != null) {
                    try {
                        contentReader.close();
                    } catch (IOException e) {
                        //ignore
                    }
                }
            }
            return null;
        }

        if (canInheritJavadoc(member)) {
            IMethod method = (IMethod) member;
            return new JavadocContentAccess2(element, javadoc, rawJavadoc,
                    new JavadocLookup(method.getDeclaringType())).toHTML();
        }
        return new JavadocContentAccess2(element, javadoc, rawJavadoc).toHTML();
    }

    private static boolean canInheritJavadoc(IMember member) {
        if (member instanceof IMethod && member.getJavaProject().exists()) {
            /*
             * Exists test catches ExternalJavaProject, in which case no hierarchy can be built.
             */
            try {
                return !((IMethod) member).isConstructor();
            } catch (JavaModelException e) {
                // TODO
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * Gets the reader content as a String
     *
     * @param reader the reader
     * @return the reader content as string
     */
    private static String getString(Reader reader) {
        StringBuffer buf = new StringBuffer();
        char[] buffer = new char[1024];
        int count;
        try {
            while ((count = reader.read(buffer)) != -1)
                buf.append(buffer, 0, count);
        } catch (IOException e) {
            return null;
        }
        return buf.toString();
    }

    /**
     * Finds the first available attached Javadoc in the hierarchy of the given method.
     *
     * @param method the method
     * @return the inherited Javadoc from the Javadoc attachment, or <code>null</code> if none
     * @throws JavaModelException unexpected problem
     */
    private static String findAttachedDocInHierarchy(final IMethod method) throws JavaModelException {
        IType type = method.getDeclaringType();
        ITypeHierarchy hierarchy = SuperTypeHierarchyCache.getTypeHierarchy(type);
        final MethodOverrideTester tester = SuperTypeHierarchyCache.getMethodOverrideTester(type);

        return (String) new InheritDocVisitor() {
            @Override
            public Object visit(IType currType) throws JavaModelException {
                IMethod overridden = tester.findOverriddenMethodInType(currType, method);
                if (overridden == null)
                    return InheritDocVisitor.CONTINUE;

                if (overridden.getOpenable().getBuffer() == null) { // only if no source available
                    String attachedJavadoc = overridden.getAttachedJavadoc(null);
                    if (attachedJavadoc != null) {
                        // BaseURL for the original method can be wrong for attached Javadoc from overridden
                        // (e.g. when overridden is from rt.jar).
                        // Fix is to store the baseURL inside the doc content and later fetch it with #extractBaseURL(String).
                        String baseURL = JavaDocLocations.getBaseURL(overridden, overridden.isBinary());
                        if (baseURL != null) {
                            attachedJavadoc = BASE_URL_COMMENT_INTRO + baseURL + "\"--> " + attachedJavadoc; //$NON-NLS-1$
                        }
                        return attachedJavadoc;
                    }
                }
                return CONTINUE;
            }
        }.visitInheritDoc(type, hierarchy);
    }

    private String toHTML() {
        fBuf = new StringBuffer();
        fLiteralContent = 0;

        if (fElement instanceof ILocalVariable || fElement instanceof ITypeParameter) {
            parameterToHTML();
        } else {
            elementToHTML();
        }

        String result = fBuf.toString();
        fBuf = null;
        return result;
    }

    private void parameterToHTML() {
        String elementName = fElement.getElementName();
        List<TagElement> tags = fJavadoc.tags();
        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            String tagName = tag.getTagName();
            if (TagElement.TAG_PARAM.equals(tagName)) {
                List<? extends ASTNode> fragments = tag.fragments();
                int size = fragments.size();
                if (size > 0) {
                    Object first = fragments.get(0);
                    if (first instanceof SimpleName) {
                        String name = ((SimpleName) first).getIdentifier();
                        if (elementName.equals(name)) {
                            handleContentElements(fragments.subList(1, size));
                            return;
                        }
                    } else if (size > 2 && fElement instanceof ITypeParameter && first instanceof TextElement) {
                        String firstText = ((TextElement) first).getText();
                        if ("<".equals(firstText)) { //$NON-NLS-1$
                            Object second = fragments.get(1);
                            Object third = fragments.get(2);
                            if (second instanceof SimpleName && third instanceof TextElement) {
                                String name = ((SimpleName) second).getIdentifier();
                                String thirdText = ((TextElement) third).getText();
                                if (elementName.equals(name) && ">".equals(thirdText)) { //$NON-NLS-1$
                                    handleContentElements(fragments.subList(3, size));
                                    return;
                                }
                            }
                        }
                    }
                }
            }
        }
        if (fElement instanceof ILocalVariable) {
            List<String> parameterNames = initParameterNames();
            int i = parameterNames.indexOf(elementName);
            if (i != -1) {
                CharSequence inheritedParamDescription = fJavadocLookup.getInheritedParamDescription(fMethod, i);
                handleInherited(inheritedParamDescription);
            }
        } else if (fElement instanceof ITypeParameter) {
            List<String> typeParameterNames = initTypeParameterNames();
            int i = typeParameterNames.indexOf(elementName);
            if (i != -1) {
                CharSequence inheritedTypeParamDescription = fJavadocLookup
                        .getInheritedTypeParamDescription(fMethod, i);
                handleInherited(inheritedTypeParamDescription);
            }
        }
    }

    private void elementToHTML() {
        // After first loop, non-null entries in the following two lists are missing and need to be inherited:
        List<String> typeParameterNames = initTypeParameterNames();
        List<String> parameterNames = initParameterNames();
        List<String> exceptionNames = initExceptionNames();

        TagElement deprecatedTag = null;
        TagElement start = null;
        List<TagElement> typeParameters = new ArrayList<>();
        List<TagElement> parameters = new ArrayList<>();
        TagElement returnTag = null;
        List<TagElement> exceptions = new ArrayList<>();
        List<TagElement> versions = new ArrayList<>();
        List<TagElement> authors = new ArrayList<>();
        List<TagElement> sees = new ArrayList<>();
        List<TagElement> since = new ArrayList<>();
        List<TagElement> rest = new ArrayList<>();

        List<TagElement> tags = fJavadoc.tags();
        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            String tagName = tag.getTagName();
            if (tagName == null) {
                start = tag;

            } else if (TagElement.TAG_PARAM.equals(tagName)) {
                List<? extends ASTNode> fragments = tag.fragments();
                int size = fragments.size();
                if (size > 0) {
                    Object first = fragments.get(0);
                    if (first instanceof SimpleName) {
                        String name = ((SimpleName) first).getIdentifier();
                        int paramIndex = parameterNames.indexOf(name);
                        if (paramIndex != -1) {
                            parameterNames.set(paramIndex, null);
                        }
                        parameters.add(tag);
                    } else if (size > 2 && first instanceof TextElement) {
                        String firstText = ((TextElement) first).getText();
                        if ("<".equals(firstText)) { //$NON-NLS-1$
                            Object second = fragments.get(1);
                            Object third = fragments.get(2);
                            if (second instanceof SimpleName && third instanceof TextElement) {
                                String name = ((SimpleName) second).getIdentifier();
                                String thirdText = ((TextElement) third).getText();
                                if (">".equals(thirdText)) { //$NON-NLS-1$
                                    int paramIndex = typeParameterNames.indexOf(name);
                                    if (paramIndex != -1) {
                                        typeParameterNames.set(paramIndex, null);
                                    }
                                    typeParameters.add(tag);
                                }
                            }
                        }
                    }
                }

            } else if (TagElement.TAG_RETURN.equals(tagName)) {
                if (returnTag == null)
                    returnTag = tag; // the Javadoc tool only shows the first return tag

            } else if (TagElement.TAG_EXCEPTION.equals(tagName) || TagElement.TAG_THROWS.equals(tagName)) {
                exceptions.add(tag);
                List<? extends ASTNode> fragments = tag.fragments();
                if (fragments.size() > 0) {
                    Object first = fragments.get(0);
                    if (first instanceof Name) {
                        String name = ASTNodes.getSimpleNameIdentifier((Name) first);
                        int exceptionIndex = exceptionNames.indexOf(name);
                        if (exceptionIndex != -1) {
                            exceptionNames.set(exceptionIndex, null);
                        }
                    }
                }

            } else if (TagElement.TAG_SINCE.equals(tagName)) {
                since.add(tag);
            } else if (TagElement.TAG_VERSION.equals(tagName)) {
                versions.add(tag);
            } else if (TagElement.TAG_AUTHOR.equals(tagName)) {
                authors.add(tag);
            } else if (TagElement.TAG_SEE.equals(tagName)) {
                sees.add(tag);
            } else if (TagElement.TAG_DEPRECATED.equals(tagName)) {
                if (deprecatedTag == null)
                    deprecatedTag = tag; // the Javadoc tool only shows the first deprecated tag
            } else {
                rest.add(tag);
            }
        }

        //TODO: @Documented annotations before header
        if (deprecatedTag != null)
            handleDeprecatedTag(deprecatedTag);
        if (start != null)
            handleContentElements(start.fragments());
        else if (fMethod != null) {
            CharSequence inherited = fJavadocLookup.getInheritedMainDescription(fMethod);
            // The Javadoc tool adds "Description copied from class: ..." (only for the main description).
            // We don't bother doing that.
            handleInherited(inherited);
        }

        CharSequence[] typeParameterDescriptions = new CharSequence[typeParameterNames.size()];
        boolean hasInheritedTypeParameters = inheritTypeParameterDescriptions(typeParameterNames,
                typeParameterDescriptions);
        boolean hasTypeParameters = typeParameters.size() > 0 || hasInheritedTypeParameters;

        CharSequence[] parameterDescriptions = new CharSequence[parameterNames.size()];
        boolean hasInheritedParameters = inheritParameterDescriptions(parameterNames, parameterDescriptions);
        boolean hasParameters = parameters.size() > 0 || hasInheritedParameters;

        CharSequence returnDescription = null;
        if (returnTag == null && needsReturnTag())
            returnDescription = fJavadocLookup.getInheritedReturnDescription(fMethod);
        boolean hasReturnTag = returnTag != null || returnDescription != null;

        CharSequence[] exceptionDescriptions = new CharSequence[exceptionNames.size()];
        boolean hasInheritedExceptions = inheritExceptionDescriptions(exceptionNames, exceptionDescriptions);
        boolean hasExceptions = exceptions.size() > 0 || hasInheritedExceptions;

        if (hasParameters || hasTypeParameters || hasReturnTag || hasExceptions || versions.size() > 0
                || authors.size() > 0 || since.size() > 0 || sees.size() > 0 || rest.size() > 0
                || (fBuf.length() > 0 && (parameterDescriptions.length > 0 || exceptionDescriptions.length > 0))) {
            handleSuperMethodReferences();
            fBuf.append(BLOCK_TAG_START);
            handleParameterTags(typeParameters, typeParameterNames, typeParameterDescriptions, true);
            handleParameterTags(parameters, parameterNames, parameterDescriptions, false);
            handleReturnTag(returnTag, returnDescription);
            handleExceptionTags(exceptions, exceptionNames, exceptionDescriptions);
            handleBlockTags(JavaDocMessages.JavaDoc2HTMLTextReader_since_section, since);
            handleBlockTags(JavaDocMessages.JavaDoc2HTMLTextReader_version_section, versions);
            handleBlockTags(JavaDocMessages.JavaDoc2HTMLTextReader_author_section, authors);
            handleBlockTags(JavaDocMessages.JavaDoc2HTMLTextReader_see_section, sees);
            handleBlockTags(rest);
            fBuf.append(BLOCK_TAG_END);

        } else if (fBuf.length() > 0) {
            handleSuperMethodReferences();
        }
    }

    private void handleDeprecatedTag(TagElement tag) {
        fBuf.append("<p><b>"); //$NON-NLS-1$
        fBuf.append(JavaDocMessages.JavaDoc2HTMLTextReader_deprecated_section);
        fBuf.append("</b> <i>"); //$NON-NLS-1$
        handleContentElements(tag.fragments());
        fBuf.append("</i><p>"); //$NON-NLS-1$ TODO: Why not </p>? See https://bugs.eclipse.org/bugs/show_bug.cgi?id=243318 .
    }

    private void handleSuperMethodReferences() {
        if (fMethod != null) {
            try {
                StringBuffer superMethodReferences = createSuperMethodReferences(fMethod);
                if (superMethodReferences != null)
                    fBuf.append(superMethodReferences);
            } catch (JavaModelException e) {
                //TODO
                e.printStackTrace();
            }
        }
    }

    private List<String> initTypeParameterNames() {
        if (fMethod != null) {
            try {
                ArrayList<String> typeParameterNames = new ArrayList<>();
                for (ITypeParameter typeParameter : fMethod.getTypeParameters()) {
                    typeParameterNames.add(typeParameter.getElementName());
                }
                return typeParameterNames;
            } catch (JavaModelException e) {
                //TODO
                e.printStackTrace();
            }
        }
        return Collections.emptyList();
    }

    private List<String> initParameterNames() {
        if (fMethod != null) {
            try {
                return new ArrayList<>(Arrays.asList(fMethod.getParameterNames()));
            } catch (JavaModelException e) {
                //TODO
                e.printStackTrace();
            }
        }
        return Collections.emptyList();
    }

    private List<String> initExceptionNames() {
        if (fMethod != null) {
            try {
                String[] exceptionTypes = fMethod.getExceptionTypes();
                ArrayList<String> exceptionNames = new ArrayList<>();
                for (int i = 0; i < exceptionTypes.length; i++) {
                    exceptionNames.add(Signature.getSimpleName(Signature.toString(exceptionTypes[i])));
                }
                return exceptionNames;
            } catch (JavaModelException e) {
                //TODO
                e.printStackTrace();
            }
        }
        return Collections.emptyList();
    }

    private boolean needsReturnTag() {
        if (fMethod == null)
            return false;
        try {
            return !Signature.SIG_VOID.equals(fMethod.getReturnType());
        } catch (JavaModelException e) {
            //TODO
            e.printStackTrace();
            return false;
        }
    }

    private boolean inheritTypeParameterDescriptions(List<String> typeParameterNames,
            CharSequence[] typeParameterDescriptions) {
        boolean hasInheritedTypeParameters = false;
        for (int i = 0; i < typeParameterNames.size(); i++) {
            String name = typeParameterNames.get(i);
            if (name != null) {
                typeParameterDescriptions[i] = fJavadocLookup.getInheritedTypeParamDescription(fMethod, i);
                if (typeParameterDescriptions[i] != null)
                    hasInheritedTypeParameters = true;
            }
        }
        return hasInheritedTypeParameters;
    }

    private boolean inheritParameterDescriptions(List<String> parameterNames,
            CharSequence[] parameterDescriptions) {
        boolean hasInheritedParameters = false;
        for (int i = 0; i < parameterNames.size(); i++) {
            String name = parameterNames.get(i);
            if (name != null) {
                parameterDescriptions[i] = fJavadocLookup.getInheritedParamDescription(fMethod, i);
                if (parameterDescriptions[i] != null)
                    hasInheritedParameters = true;
            }
        }
        return hasInheritedParameters;
    }

    private boolean inheritExceptionDescriptions(List<String> exceptionNames,
            CharSequence[] exceptionDescriptions) {
        boolean hasInheritedExceptions = false;
        for (int i = 0; i < exceptionNames.size(); i++) {
            String name = exceptionNames.get(i);
            if (name != null) {
                exceptionDescriptions[i] = fJavadocLookup.getInheritedExceptionDescription(fMethod, name);
                if (exceptionDescriptions[i] != null)
                    hasInheritedExceptions = true;
            }
        }
        return hasInheritedExceptions;
    }

    CharSequence getMainDescription() {
        if (fMainDescription == null) {
            fMainDescription = new StringBuffer();
            fBuf = fMainDescription;
            fLiteralContent = 0;

            List<TagElement> tags = fJavadoc.tags();
            for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
                TagElement tag = iter.next();
                String tagName = tag.getTagName();
                if (tagName == null) {
                    handleContentElements(tag.fragments());
                    break;
                }
            }

            fBuf = null;
        }
        return fMainDescription.length() > 0 ? fMainDescription : null;
    }

    CharSequence getReturnDescription() {
        if (fReturnDescription == null) {
            fReturnDescription = new StringBuffer();
            fBuf = fReturnDescription;
            fLiteralContent = 0;

            List<TagElement> tags = fJavadoc.tags();
            for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
                TagElement tag = iter.next();
                String tagName = tag.getTagName();
                if (TagElement.TAG_RETURN.equals(tagName)) {
                    handleContentElements(tag.fragments());
                    break;
                }
            }

            fBuf = null;
        }
        return fReturnDescription.length() > 0 ? fReturnDescription : null;
    }

    CharSequence getInheritedTypeParamDescription(int typeParamIndex) {
        if (fMethod != null) {
            List<String> typeParameterNames = initTypeParameterNames();
            if (fTypeParamDescriptions == null) {
                fTypeParamDescriptions = new StringBuffer[typeParameterNames.size()];
            } else {
                StringBuffer description = fTypeParamDescriptions[typeParamIndex];
                if (description != null) {
                    return description.length() > 0 ? description : null;
                }
            }

            StringBuffer description = new StringBuffer();
            fTypeParamDescriptions[typeParamIndex] = description;
            fBuf = description;
            fLiteralContent = 0;

            String typeParamName = typeParameterNames.get(typeParamIndex);
            List<TagElement> tags = fJavadoc.tags();
            for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
                TagElement tag = iter.next();
                String tagName = tag.getTagName();
                if (TagElement.TAG_PARAM.equals(tagName)) {
                    List<? extends ASTNode> fragments = tag.fragments();
                    if (fragments.size() > 2) {
                        Object first = fragments.get(0);
                        Object second = fragments.get(1);
                        Object third = fragments.get(2);
                        if (first instanceof TextElement && second instanceof SimpleName
                                && third instanceof TextElement) {
                            String firstText = ((TextElement) first).getText();
                            String thirdText = ((TextElement) third).getText();
                            if ("<".equals(firstText) && ">".equals(thirdText)) { //$NON-NLS-1$ //$NON-NLS-2$
                                String name = ((SimpleName) second).getIdentifier();
                                if (name.equals(typeParamName)) {
                                    handleContentElements(fragments.subList(3, fragments.size()));
                                    break;
                                }
                            }
                        }
                    }
                }
            }

            fBuf = null;
            return description.length() > 0 ? description : null;
        }
        return null;
    }

    CharSequence getInheritedParamDescription(int paramIndex) throws JavaModelException {
        if (fMethod != null) {
            String[] parameterNames = fMethod.getParameterNames();
            if (fParamDescriptions == null) {
                fParamDescriptions = new StringBuffer[parameterNames.length];
            } else {
                StringBuffer description = fParamDescriptions[paramIndex];
                if (description != null) {
                    return description.length() > 0 ? description : null;
                }
            }

            StringBuffer description = new StringBuffer();
            fParamDescriptions[paramIndex] = description;
            fBuf = description;
            fLiteralContent = 0;

            String paramName = parameterNames[paramIndex];
            List<TagElement> tags = fJavadoc.tags();
            for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
                TagElement tag = iter.next();
                String tagName = tag.getTagName();
                if (TagElement.TAG_PARAM.equals(tagName)) {
                    List<? extends ASTNode> fragments = tag.fragments();
                    if (fragments.size() > 0) {
                        Object first = fragments.get(0);
                        if (first instanceof SimpleName) {
                            String name = ((SimpleName) first).getIdentifier();
                            if (name.equals(paramName)) {
                                handleContentElements(fragments.subList(1, fragments.size()));
                                break;
                            }
                        }
                    }
                }
            }

            fBuf = null;
            return description.length() > 0 ? description : null;
        }
        return null;
    }

    CharSequence getExceptionDescription(String simpleName) {
        if (fMethod != null) {
            if (fExceptionDescriptions == null) {
                fExceptionDescriptions = new HashMap<>();
            } else {
                StringBuffer description = fExceptionDescriptions.get(simpleName);
                if (description != null) {
                    return description.length() > 0 ? description : null;
                }
            }

            StringBuffer description = new StringBuffer();
            fExceptionDescriptions.put(simpleName, description);
            fBuf = description;
            fLiteralContent = 0;

            List<TagElement> tags = fJavadoc.tags();
            for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
                TagElement tag = iter.next();
                String tagName = tag.getTagName();
                if (TagElement.TAG_THROWS.equals(tagName) || TagElement.TAG_EXCEPTION.equals(tagName)) {
                    List<? extends ASTNode> fragments = tag.fragments();
                    if (fragments.size() > 0) {
                        Object first = fragments.get(0);
                        if (first instanceof Name) {
                            String name = ASTNodes.getSimpleNameIdentifier((Name) first);
                            if (name.equals(simpleName)) {
                                if (fragments.size() > 1)
                                    handleContentElements(fragments.subList(1, fragments.size()));
                                break;
                            }
                        }
                    }
                }
            }

            fBuf = null;
            return description.length() > 0 ? description : null;
        }
        return null;
    }

    private void handleContentElements(List<? extends ASTNode> nodes) {
        handleContentElements(nodes, false);
    }

    private void handleContentElements(List<? extends ASTNode> nodes, boolean skipLeadingWhitespace) {
        ASTNode previousNode = null;
        for (Iterator<? extends ASTNode> iter = nodes.iterator(); iter.hasNext();) {
            ASTNode child = iter.next();
            if (previousNode != null) {
                int previousEnd = previousNode.getStartPosition() + previousNode.getLength();
                int childStart = child.getStartPosition();
                if (previousEnd > childStart) {
                    // should never happen, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=304826
                    Exception exception = new Exception("Illegal ASTNode positions: previousEnd=" + previousEnd //$NON-NLS-1$
                            + ", childStart=" + childStart //$NON-NLS-1$
                            + ", element=" + fElement.getHandleIdentifier() //$NON-NLS-1$
                            + ", Javadoc:\n" + fSource); //$NON-NLS-1$
                    //TODO
                    exception.printStackTrace();
                } else if (previousEnd != childStart) {
                    // Need to preserve whitespace before a node that's not
                    // directly following the previous node (e.g. on a new line)
                    // due to https://bugs.eclipse.org/bugs/show_bug.cgi?id=206518 :
                    String textWithStars = fSource.substring(previousEnd, childStart);
                    String text = removeDocLineIntros(textWithStars);
                    fBuf.append(text);
                }
            }
            previousNode = child;
            if (child instanceof TextElement) {
                String text = ((TextElement) child).getText();
                if (skipLeadingWhitespace) {
                    text = text.replaceFirst("^\\s+", ""); //$NON-NLS-1$ //$NON-NLS-2$
                }
                // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=233481 :
                text = text.replaceAll("(\r\n?|\n)([ \t]*\\*)", "$1"); //$NON-NLS-1$ //$NON-NLS-2$
                text = text.replace("#__", "}");
                text = text.replace("__#", "{");

                handleText(text);

            } else if (child instanceof TagElement) {
                handleInlineTagElement((TagElement) child);
            } else {
                // This is unexpected. Fail gracefully by just copying the source.
                int start = child.getStartPosition();
                String text = fSource.substring(start, start + child.getLength());
                fBuf.append(removeDocLineIntros(text));
            }
        }
    }

    private String removeDocLineIntros(String textWithStars) {
        String lineBreakGroup = "(\\r\\n?|\\n)"; //$NON-NLS-1$
        String noBreakSpace = "[^\r\n&&\\s]"; //$NON-NLS-1$
        return textWithStars.replaceAll(lineBreakGroup + noBreakSpace + "*\\*" /*+ noBreakSpace + '?'*/, "$1"); //$NON-NLS-1$ //$NON-NLS-2$
    }

    private void handleText(String text) {
        if (fLiteralContent == 0) {
            fBuf.append(text);
        } else {
            appendEscaped(fBuf, text);
        }
    }

    private static void appendEscaped(StringBuffer buf, String text) {
        int nextToCopy = 0;
        int length = text.length();
        for (int i = 0; i < length; i++) {
            char ch = text.charAt(i);
            String rep = null;
            switch (ch) {
            case '&':
                rep = "&amp;"; //$NON-NLS-1$
                break;
            case '"':
                rep = "&quot;"; //$NON-NLS-1$
                break;
            case '<':
                rep = "&lt;"; //$NON-NLS-1$
                break;
            case '>':
                rep = "&gt;"; //$NON-NLS-1$
                break;
            }
            if (rep != null) {
                if (nextToCopy < i)
                    buf.append(text.substring(nextToCopy, i));
                buf.append(rep);
                nextToCopy = i + 1;
            }
        }
        if (nextToCopy < length)
            buf.append(text.substring(nextToCopy));
    }

    private void handleInlineTagElement(TagElement node) {
        String name = node.getTagName();

        if (TagElement.TAG_VALUE.equals(name) && handleValueTag(node))
            return;

        boolean isLink = TagElement.TAG_LINK.equals(name);
        boolean isLinkplain = TagElement.TAG_LINKPLAIN.equals(name);
        boolean isCode = TagElement.TAG_CODE.equals(name);
        boolean isLiteral = TagElement.TAG_LITERAL.equals(name);

        if (isLiteral || isCode)
            fLiteralContent++;
        if (isLink || isCode)
            fBuf.append("<code>"); //$NON-NLS-1$

        if (isLink || isLinkplain)
            handleLink(node.fragments());
        else if (isCode || isLiteral)
            handleContentElements(node.fragments(), true);
        else if (handleInheritDoc(node)) {
            // handled
        } else if (handleDocRoot(node)) {
            // handled
        } else {
            //print uninterpreted source {@tagname ...} for unknown tags
            int start = node.getStartPosition();
            String text = fSource.substring(start, start + node.getLength());
            fBuf.append(removeDocLineIntros(text));
        }

        if (isLink || isCode)
            fBuf.append("</code>"); //$NON-NLS-1$
        if (isLiteral || isCode)
            fLiteralContent--;

    }

    private boolean handleValueTag(TagElement node) {

        List<? extends ASTNode> fragments = node.fragments();
        try {
            if (!(fElement instanceof IMember)) {
                return false;
            }
            if (fragments.isEmpty()) {
                if (fElement instanceof IField && JdtFlags.isStatic((IField) fElement)
                        && JdtFlags.isFinal((IField) fElement)) {
                    IField field = (IField) fElement;
                    return handleConstantValue(field, false);
                }
            } else if (fragments.size() == 1) {
                Object first = fragments.get(0);
                if (first instanceof MemberRef) {
                    MemberRef memberRef = (MemberRef) first;
                    if (memberRef.getQualifier() == null) {
                        SimpleName name = memberRef.getName();
                        IType type = fElement instanceof IType ? (IType) fElement
                                : ((IMember) fElement).getDeclaringType();
                        while (type != null) {
                            IField field = type.getField(name.getIdentifier());
                            if (field != null && field.exists()) {
                                if (JdtFlags.isStatic(field) && JdtFlags.isFinal(field))
                                    return handleConstantValue(field, true);
                                break;
                            }
                            type = type.getDeclaringType();
                        }
                    }
                }
            }
        } catch (JavaModelException e) {
            //TODO
            e.printStackTrace();
        }

        return false;
    }

    private boolean handleConstantValue(IField field, boolean link) throws JavaModelException {
        //      String text= null;
        //
        //      ISourceRange nameRange= field.getNameRange();
        //      if (SourceRange.isAvailable(nameRange)) {
        //         CompilationUnit cuNode= SharedASTProvider.getAST(field.getTypeRoot(), SharedASTProvider.WAIT_ACTIVE_ONLY, null);
        //         if (cuNode != null) {
        //            ASTNode nameNode= NodeFinder.perform(cuNode, nameRange);
        //            if (nameNode instanceof SimpleName) {
        //               IBinding binding= ((SimpleName) nameNode).resolveBinding();
        //               if (binding instanceof IVariableBinding) {
        //                  IVariableBinding variableBinding= (IVariableBinding) binding;
        //                  Object constantValue= variableBinding.getConstantValue();
        //                  if (constantValue != null) {
        //                     if (constantValue instanceof String) {
        //                        text= ASTNodes.getEscapedStringLiteral((String) constantValue);
        //                     } else {
        //                        text= constantValue.toString(); // Javadoc tool is even worse for chars...
        //                     }
        //                  }
        //               }
        //            }
        //         }
        //      }
        //
        //      if (text == null) {
        //         Object constant= field.getConstant();
        //         if (constant != null) {
        //            text= constant.toString();
        //         }
        //      }
        //
        //      if (text != null) {
        //         text= HTMLPrinter.convertToHTMLContentWithWhitespace(text);
        //         if (link) {
        //            String uri;
        //            try {
        //               uri= JavaElementLinks.createURI(JavaElementLinks.JAVADOC_SCHEME, field);
        //               fBuf.append(JavaElementLinks.createLink(uri, text));
        //            } catch (URISyntaxException e) {
        //               JavaPlugin.log(e);
        //               return false;
        //            }
        //         } else {
        //            handleText(text);
        //         }
        //         return true;
        //      }
        return false;
    }

    private boolean handleDocRoot(TagElement node) {
        if (!TagElement.TAG_DOCROOT.equals(node.getTagName()))
            return false;

        try {
            String url = null;
            if (fElement instanceof IMember && ((IMember) fElement).isBinary()) {
                URL javadocBaseLocation = JavaDocLocations.getJavadocBaseLocation(fElement);
                if (javadocBaseLocation != null) {
                    url = javadocBaseLocation.toExternalForm();
                }
            } else {
                IPackageFragmentRoot srcRoot = JavaModelUtil.getPackageFragmentRoot(fElement);
                if (srcRoot != null) {
                    IResource resource = srcRoot.getResource();
                    if (resource != null) {
                        /*
                         * Too bad: Browser widget knows nothing about EFS and custom URL handlers,
                         * so IResource#getLocationURI() does not work in all cases.
                         * We only support the local file system for now.
                         * A solution could be https://bugs.eclipse.org/bugs/show_bug.cgi?id=149022 .
                         */
                        IPath location = resource.getLocation();
                        if (location != null) {
                            url = location.toFile().toURI().toASCIIString();
                        }
                    }

                }
            }
            if (url != null) {
                if (url.endsWith("/")) { //$NON-NLS-1$
                    url = url.substring(0, url.length() - 1);
                }
                fBuf.append(url);
                return true;
            }
        } catch (JavaModelException e) {
        }
        return false;
    }

    /**
     * Handle {&#64;inheritDoc}.
     *
     * @param node the node
     * @return <code>true</code> iff the node was an {&#64;inheritDoc} node and has been handled
     */
    private boolean handleInheritDoc(TagElement node) {
        if (!TagElement.TAG_INHERITDOC.equals(node.getTagName()))
            return false;
        try {
            if (fMethod == null)
                return false;

            TagElement blockTag = (TagElement) node.getParent();
            String blockTagName = blockTag.getTagName();

            if (blockTagName == null) {
                CharSequence inherited = fJavadocLookup.getInheritedMainDescription(fMethod);
                return handleInherited(inherited);

            } else if (TagElement.TAG_PARAM.equals(blockTagName)) {
                List<? extends ASTNode> fragments = blockTag.fragments();
                int size = fragments.size();
                if (size > 0) {
                    Object first = fragments.get(0);
                    if (first instanceof SimpleName) {
                        String name = ((SimpleName) first).getIdentifier();
                        String[] parameterNames = fMethod.getParameterNames();
                        for (int i = 0; i < parameterNames.length; i++) {
                            if (name.equals(parameterNames[i])) {
                                CharSequence inherited = fJavadocLookup.getInheritedParamDescription(fMethod, i);
                                return handleInherited(inherited);
                            }
                        }
                    } else if (size > 2 && first instanceof TextElement) {
                        String firstText = ((TextElement) first).getText();
                        if ("<".equals(firstText)) { //$NON-NLS-1$
                            Object second = fragments.get(1);
                            Object third = fragments.get(2);
                            if (second instanceof SimpleName && third instanceof TextElement) {
                                String thirdText = ((TextElement) third).getText();
                                if (">".equals(thirdText)) { //$NON-NLS-1$
                                    String name = ((SimpleName) second).getIdentifier();
                                    ITypeParameter[] typeParameters = fMethod.getTypeParameters();
                                    for (int i = 0; i < typeParameters.length; i++) {
                                        ITypeParameter typeParameter = typeParameters[i];
                                        if (name.equals(typeParameter.getElementName())) {
                                            CharSequence inherited = fJavadocLookup
                                                    .getInheritedTypeParamDescription(fMethod, i);
                                            return handleInherited(inherited);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

            } else if (TagElement.TAG_RETURN.equals(blockTagName)) {
                CharSequence inherited = fJavadocLookup.getInheritedReturnDescription(fMethod);
                return handleInherited(inherited);

            } else if (TagElement.TAG_THROWS.equals(blockTagName)
                    || TagElement.TAG_EXCEPTION.equals(blockTagName)) {
                List<? extends ASTNode> fragments = blockTag.fragments();
                if (fragments.size() > 0) {
                    Object first = fragments.get(0);
                    if (first instanceof Name) {
                        String name = ASTNodes.getSimpleNameIdentifier((Name) first);
                        CharSequence inherited = fJavadocLookup.getInheritedExceptionDescription(fMethod, name);
                        return handleInherited(inherited);
                    }
                }
            }
        } catch (JavaModelException e) {
            //TODO
            e.printStackTrace();
        }
        return false;
    }

    private boolean handleInherited(CharSequence inherited) {
        if (inherited == null)
            return false;

        fBuf.append(inherited);
        return true;
    }

    private void handleBlockTags(String title, List<TagElement> tags) {
        if (tags.isEmpty())
            return;

        handleBlockTagTitle(title);

        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            fBuf.append(BlOCK_TAG_ENTRY_START);
            if (TagElement.TAG_SEE.equals(tag.getTagName())) {
                handleSeeTag(tag);
            } else {
                handleContentElements(tag.fragments());
            }
            fBuf.append(BlOCK_TAG_ENTRY_END);
        }
    }

    private void handleReturnTag(TagElement tag, CharSequence returnDescription) {
        if (tag == null && returnDescription == null)
            return;

        handleBlockTagTitle(JavaDocMessages.JavaDoc2HTMLTextReader_returns_section);
        fBuf.append(BlOCK_TAG_ENTRY_START);
        if (tag != null)
            handleContentElements(tag.fragments());
        else
            fBuf.append(returnDescription);
        fBuf.append(BlOCK_TAG_ENTRY_END);
    }

    private void handleBlockTags(List<TagElement> tags) {
        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            handleBlockTagTitle(tag.getTagName());
            fBuf.append(BlOCK_TAG_ENTRY_START);
            handleContentElements(tag.fragments());
            fBuf.append(BlOCK_TAG_ENTRY_END);
        }
    }

    private void handleBlockTagTitle(String title) {
        fBuf.append("<dt>"); //$NON-NLS-1$
        fBuf.append(title);
        fBuf.append("</dt>"); //$NON-NLS-1$
    }

    private void handleSeeTag(TagElement tag) {
        handleLink(tag.fragments());
    }

    private void handleExceptionTags(List<TagElement> tags, List<String> exceptionNames,
            CharSequence[] exceptionDescriptions) {
        if (tags.size() == 0 && containsOnlyNull(exceptionNames))
            return;

        handleBlockTagTitle(JavaDocMessages.JavaDoc2HTMLTextReader_throws_section);

        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            fBuf.append(BlOCK_TAG_ENTRY_START);
            handleThrowsTag(tag);
            fBuf.append(BlOCK_TAG_ENTRY_END);
        }
        for (int i = 0; i < exceptionDescriptions.length; i++) {
            CharSequence description = exceptionDescriptions[i];
            String name = exceptionNames.get(i);
            if (name != null) {
                fBuf.append(BlOCK_TAG_ENTRY_START);
                handleLink(Collections.singletonList(fJavadoc.getAST().newSimpleName(name)));
                if (description != null) {
                    fBuf.append(JavaElementLabels.CONCAT_STRING);
                    fBuf.append(description);
                }
                fBuf.append(BlOCK_TAG_ENTRY_END);
            }
        }
    }

    private void handleThrowsTag(TagElement tag) {
        List<? extends ASTNode> fragments = tag.fragments();
        int size = fragments.size();
        if (size > 0) {
            handleLink(fragments.subList(0, 1));
            if (size > 1) {
                fBuf.append(JavaElementLabels.CONCAT_STRING);
                handleContentElements(fragments.subList(1, size));
            }
        }
    }

    private void handleParameterTags(List<TagElement> tags, List<String> parameterNames,
            CharSequence[] parameterDescriptions, boolean isTypeParameters) {
        if (tags.size() == 0 && containsOnlyNull(parameterNames))
            return;

        String tagTitle = isTypeParameters ? JavaDocMessages.JavaDoc2HTMLTextReader_type_parameters_section
                : JavaDocMessages.JavaDoc2HTMLTextReader_parameters_section;
        handleBlockTagTitle(tagTitle);

        for (Iterator<TagElement> iter = tags.iterator(); iter.hasNext();) {
            TagElement tag = iter.next();
            fBuf.append(BlOCK_TAG_ENTRY_START);
            handleParamTag(tag);
            fBuf.append(BlOCK_TAG_ENTRY_END);
        }
        for (int i = 0; i < parameterDescriptions.length; i++) {
            CharSequence description = parameterDescriptions[i];
            String name = parameterNames.get(i);
            if (name != null) {
                fBuf.append(BlOCK_TAG_ENTRY_START);
                fBuf.append(PARAM_NAME_START);
                if (isTypeParameters) {
                    fBuf.append("&lt;"); //$NON-NLS-1$
                }
                fBuf.append(name);
                if (isTypeParameters) {
                    fBuf.append("&gt;"); //$NON-NLS-1$
                }
                fBuf.append(PARAM_NAME_END);
                if (description != null)
                    fBuf.append(description);
                fBuf.append(BlOCK_TAG_ENTRY_END);
            }
        }
    }

    private void handleParamTag(TagElement tag) {
        List<? extends ASTNode> fragments = tag.fragments();
        int i = 0;
        int size = fragments.size();
        if (size > 0) {
            Object first = fragments.get(0);
            fBuf.append(PARAM_NAME_START);
            if (first instanceof SimpleName) {
                String name = ((SimpleName) first).getIdentifier();
                fBuf.append(name);
                i++;
            } else if (first instanceof TextElement) {
                String firstText = ((TextElement) first).getText();
                if ("<".equals(firstText)) { //$NON-NLS-1$
                    fBuf.append("&lt;"); //$NON-NLS-1$
                    i++;
                    if (size > 1) {
                        Object second = fragments.get(1);
                        if (second instanceof SimpleName) {
                            String name = ((SimpleName) second).getIdentifier();
                            fBuf.append(name);
                            i++;
                            if (size > 2) {
                                Object third = fragments.get(2);
                                String thirdText = ((TextElement) third).getText();
                                if (">".equals(thirdText)) { //$NON-NLS-1$
                                    fBuf.append("&gt;"); //$NON-NLS-1$
                                    i++;
                                }
                            }
                        }
                    }
                }
            }
            fBuf.append(PARAM_NAME_END);

            handleContentElements(fragments.subList(i, fragments.size()));
        }
    }

    private void handleLink(List<? extends ASTNode> fragments) {
        //TODO: Javadoc shortens type names to minimal length according to context
        int fs = fragments.size();
        if (fs > 0) {
            Object first = fragments.get(0);
            String refTypeName = null;
            String refMemberName = null;
            String[] refMethodParamTypes = null;
            String[] refMethodParamNames = null;
            if (first instanceof Name) {
                Name name = (Name) first;
                refTypeName = name.getFullyQualifiedName();
            } else if (first instanceof MemberRef) {
                MemberRef memberRef = (MemberRef) first;
                Name qualifier = memberRef.getQualifier();
                refTypeName = qualifier == null ? "" : qualifier.getFullyQualifiedName(); //$NON-NLS-1$
                refMemberName = memberRef.getName().getIdentifier();
            } else if (first instanceof MethodRef) {
                MethodRef methodRef = (MethodRef) first;
                Name qualifier = methodRef.getQualifier();
                refTypeName = qualifier == null ? "" : qualifier.getFullyQualifiedName(); //$NON-NLS-1$
                refMemberName = methodRef.getName().getIdentifier();
                List<MethodRefParameter> params = methodRef.parameters();
                int ps = params.size();
                refMethodParamTypes = new String[ps];
                refMethodParamNames = new String[ps];
                for (int i = 0; i < ps; i++) {
                    MethodRefParameter param = params.get(i);
                    refMethodParamTypes[i] = ASTNodes.asString(param.getType());
                    SimpleName paramName = param.getName();
                    if (paramName != null)
                        refMethodParamNames[i] = paramName.getIdentifier();
                }
            }

            if (refTypeName != null) {
                fBuf.append("<a href='"); //$NON-NLS-1$
                try {
                    String scheme = JavaElementLinks.JAVADOC_SCHEME;
                    String uri = JavaElementLinks.createURI(scheme, fElement, refTypeName, refMemberName,
                            refMethodParamTypes);
                    fBuf.append(uri);
                } catch (URISyntaxException e) {
                    //TODO
                    e.printStackTrace();
                }
                fBuf.append("'>"); //$NON-NLS-1$
                if (fs > 1 && !(fs == 2 && isWhitespaceTextElement(fragments.get(1)))) {
                    handleContentElements(fragments.subList(1, fs), true);
                } else {
                    fBuf.append(refTypeName);
                    if (refMemberName != null) {
                        if (refTypeName.length() > 0) {
                            fBuf.append('.');
                        }
                        fBuf.append(refMemberName);
                        if (refMethodParamTypes != null) {
                            fBuf.append('(');
                            for (int i = 0; i < refMethodParamTypes.length; i++) {
                                String pType = refMethodParamTypes[i];
                                fBuf.append(pType);
                                String pName = refMethodParamNames[i];
                                if (pName != null) {
                                    fBuf.append(' ').append(pName);
                                }
                                if (i < refMethodParamTypes.length - 1) {
                                    fBuf.append(", "); //$NON-NLS-1$
                                }
                            }
                            fBuf.append(')');
                        }
                    }
                }
                fBuf.append("</a>"); //$NON-NLS-1$
            } else {
                handleContentElements(fragments);
            }
        }
    }

    private static boolean isWhitespaceTextElement(Object fragment) {
        if (!(fragment instanceof TextElement))
            return false;

        TextElement textElement = (TextElement) fragment;
        return textElement.getText().trim().length() == 0;
    }

    private boolean containsOnlyNull(List<String> parameterNames) {
        for (Iterator<String> iter = parameterNames.iterator(); iter.hasNext();) {
            if (iter.next() != null)
                return false;
        }
        return true;
    }

    /**
     * @param content HTML content produced by <code>getHTMLContent(...)</code>
     * @return the baseURL to use for the given content, or <code>null</code> if none
     * @since 3.10
     */
    public static String extractBaseURL(String content) {
        int introStart = content.indexOf(BASE_URL_COMMENT_INTRO);
        if (introStart != -1) {
            int introLength = BASE_URL_COMMENT_INTRO.length();
            int endIndex = content.indexOf('"', introStart + introLength);
            if (endIndex != -1) {
                return content.substring(introStart + introLength, endIndex);
            }
        }
        return null;
    }

    /**
     * Returns the Javadoc for a PackageDeclaration.
     *
     * @param packageDeclaration the Java element whose Javadoc has to be retrieved
     * @return the package documentation in HTML format or <code>null</code> if there is no
     *         associated Javadoc
     * @throws CoreException if the Java element does not exists or an exception occurs while
     *             accessing the file containing the package Javadoc
     * @since 3.9
     */
    public static String getHTMLContent(IPackageDeclaration packageDeclaration) throws CoreException {
        IJavaElement element = packageDeclaration.getAncestor(IJavaElement.PACKAGE_FRAGMENT);
        if (element instanceof IPackageFragment) {
            return getHTMLContent((IPackageFragment) element);
        }
        return null;
    }

    /**
     * Returns the Javadoc for a package which could be present in package.html, package-info.java
     * or from an attached Javadoc.
     *
     * @param packageFragment the package which is requesting for the document
     * @return the document content in HTML format or <code>null</code> if there is no associated
     *         Javadoc
     * @throws CoreException if the Java element does not exists or an exception occurs while
     *             accessing the file containing the package Javadoc
     * @since 3.9
     */
    public static String getHTMLContent(IPackageFragment packageFragment) throws CoreException {
        IPackageFragmentRoot root = (IPackageFragmentRoot) packageFragment
                .getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);

        //1==> Handle the case when the documentation is present in package-info.java or package-info.class file
        ITypeRoot packageInfo;
        boolean isBinary = root.getKind() == IPackageFragmentRoot.K_BINARY;
        if (isBinary) {
            packageInfo = packageFragment.getClassFile(JavaModelUtil.PACKAGE_INFO_CLASS);
        } else {
            packageInfo = packageFragment.getCompilationUnit(JavaModelUtil.PACKAGE_INFO_JAVA);
        }
        if (packageInfo != null && packageInfo.exists()) {
            String cuSource = packageInfo.getSource();
            //the source can be null for some of the class files
            if (cuSource != null) {
                Javadoc packageJavadocNode = getPackageJavadocNode(packageFragment, cuSource);
                if (packageJavadocNode != null) {
                    IJavaElement element;
                    if (isBinary) {
                        element = ((IClassFile) packageInfo).getType();
                    } else {
                        element = packageInfo.getParent(); // parent is the IPackageFragment
                    }
                    return new JavadocContentAccess2(element, packageJavadocNode, cuSource).toHTML();
                }
            }
        }

        // 2==> Handle the case when the documentation is done in package.html file. The file can be either in normal source folder or coming from a jar file
        else {
            Object[] nonJavaResources = packageFragment.getNonJavaResources();
            // 2.1 ==>If the package.html file is present in the source or directly in the binary jar
            for (Object nonJavaResource : nonJavaResources) {
                if (nonJavaResource instanceof IFile) {
                    IFile iFile = (IFile) nonJavaResource;
                    if (iFile.exists() && JavaModelUtil.PACKAGE_HTML.equals(iFile.getName())) {
                        return getIFileContent(iFile);
                    }
                }
            }

            // 2.2==>The file is present in a binary container
            if (isBinary) {
                for (Object nonJavaResource : nonJavaResources) {
                    // The content is from an external binary class folder
                    if (nonJavaResource instanceof IJarEntryResource) {
                        IJarEntryResource jarEntryResource = (IJarEntryResource) nonJavaResource;
                        String encoding = getSourceAttachmentEncoding(root);
                        if (JavaModelUtil.PACKAGE_HTML.equals(jarEntryResource.getName())
                                && jarEntryResource.isFile()) {
                            return getHTMLContent(jarEntryResource, encoding);
                        }
                    }
                }
                //2.3 ==>The file is present in the source attachment path.
                String contents = getHTMLContentFromAttachedSource(root, packageFragment);
                if (contents != null)
                    return contents;
            }
        }

        //3==> Handle the case when the documentation is coming from the attached Javadoc
        if ((root.isArchive() || root.isExternal())) {
            return packageFragment.getAttachedJavadoc(null);

        }

        return null;
    }

    private static String getHTMLContent(IJarEntryResource jarEntryResource, String encoding) throws CoreException {
        InputStream in = jarEntryResource.getContents();
        try {
            return getContentsFromInputStream(in, encoding);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    //ignore
                }
            }
        }
    }

    private static String getHTMLContentFromAttachedSource(IPackageFragmentRoot root,
            IPackageFragment packageFragment) throws CoreException {
        String filePath = packageFragment.getElementName().replace('.', '/') + '/'
                + JavaModelUtil.PACKAGE_INFO_JAVA;
        String contents = getFileContentFromAttachedSource(root, filePath);
        if (contents != null) {
            Javadoc packageJavadocNode = getPackageJavadocNode(packageFragment, contents);
            if (packageJavadocNode != null)
                return new JavadocContentAccess2(packageFragment, packageJavadocNode, contents).toHTML();

        }
        filePath = packageFragment.getElementName().replace('.', '/') + '/' + JavaModelUtil.PACKAGE_HTML;
        return getFileContentFromAttachedSource(root, filePath);
    }

    private static String getFileContentFromAttachedSource(IPackageFragmentRoot root, String filePath)
            throws CoreException {
        IPath sourceAttachmentPath = root.getSourceAttachmentPath();
        if (sourceAttachmentPath != null) {
            File file = null;
            String encoding = null;

            if (sourceAttachmentPath.getDevice() == null) {
                //the path could be a workspace relative path to a zip or to the source folder
                IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
                IResource res = wsRoot.findMember(sourceAttachmentPath);

                if (res instanceof IFile) {
                    // zip in the workspace
                    IPath location = res.getLocation();
                    if (location == null)
                        return null;
                    file = location.toFile();
                    encoding = ((IFile) res).getCharset(false);

                } else if (res instanceof IContainer) {
                    // folder in the workspace
                    res = ((IContainer) res).findMember(filePath);
                    if (!(res instanceof IFile))
                        return null;
                    encoding = ((IFile) res).getCharset(false);
                    if (encoding == null)
                        encoding = getSourceAttachmentEncoding(root);
                    return getContentsFromInputStream(((IFile) res).getContents(), encoding);
                }
            }

            if (file == null || !file.exists())
                file = sourceAttachmentPath.toFile();

            if (file.isDirectory()) {
                //the path is an absolute filesystem path to the source folder
                IPath packagedocPath = sourceAttachmentPath.append(filePath);
                if (packagedocPath.toFile().exists())
                    return getFileContent(packagedocPath.toFile());

            } else if (file.exists()) {
                //the package documentation is in a Jar/Zip
                IPath sourceAttachmentRootPath = root.getSourceAttachmentRootPath();
                String packagedocPath;
                //consider the root path also in the search path if it exists
                if (sourceAttachmentRootPath != null) {
                    packagedocPath = sourceAttachmentRootPath.append(filePath).toString();
                } else {
                    packagedocPath = filePath;
                }
                ZipFile zipFile = null;
                InputStream in = null;
                try {
                    zipFile = new ZipFile(file, ZipFile.OPEN_READ);
                    ZipEntry packagedocFile = zipFile.getEntry(packagedocPath);
                    if (packagedocFile != null) {
                        in = zipFile.getInputStream(packagedocFile);
                        if (encoding == null)
                            encoding = getSourceAttachmentEncoding(root);
                        return getContentsFromInputStream(in, encoding);
                    }
                } catch (IOException e) {
                    throw new CoreException(
                            new Status(IStatus.ERROR, "at.bestsolution.fxide.jdt.text", e.getMessage(), e));
                } finally {
                    try {
                        if (in != null) {
                            in.close();
                        }
                    } catch (IOException e) {
                        //ignore
                    }
                    try {
                        if (zipFile != null) {
                            zipFile.close();//this will close the InputStream also
                        }
                    } catch (IOException e) {
                        //ignore
                    }
                }
            }
        }

        return null;
    }

    private static String getContentsFromInputStream(InputStream in, String encoding) throws CoreException {
        final int defaultFileSize = 15 * 1024;
        StringBuffer buffer = new StringBuffer(defaultFileSize);
        Reader reader = null;

        try {
            reader = new BufferedReader(new InputStreamReader(in, encoding), defaultFileSize);

            char[] readBuffer = new char[2048];
            int charCount = reader.read(readBuffer);

            while (charCount > 0) {
                buffer.append(readBuffer, 0, charCount);
                charCount = reader.read(readBuffer);
            }

        } catch (IOException e) {
            throw new CoreException(new Status(IStatus.ERROR, "at.bestsolution.fxide.jdt.text", e.getMessage(), e));
        } finally {
            try {
                if (reader != null) {
                    reader.close();//this will also close the InputStream wrapped in the reader
                }
            } catch (IOException e) {
                //ignore
            }
        }
        return buffer.toString();
    }

    private static String getSourceAttachmentEncoding(IPackageFragmentRoot root) throws JavaModelException {
        String encoding = ResourcesPlugin.getEncoding();
        IClasspathEntry entry = root.getRawClasspathEntry();

        if (entry != null) {
            int kind = entry.getEntryKind();
            if (kind == IClasspathEntry.CPE_LIBRARY || kind == IClasspathEntry.CPE_VARIABLE) {
                IClasspathAttribute[] extraAttributes = entry.getExtraAttributes();
                for (int i = 0; i < extraAttributes.length; i++) {
                    IClasspathAttribute attrib = extraAttributes[i];
                    if (IClasspathAttribute.SOURCE_ATTACHMENT_ENCODING.equals(attrib.getName())) {
                        return attrib.getValue();
                    }
                }
            }
        }

        return encoding;
    }

    /**
     * Reads the content of the IFile.
     *
     * @param file the file whose content has to be read
     * @return the content of the file
     * @throws CoreException if the file could not be successfully connected or disconnected
     */
    private static String getIFileContent(IFile file) throws CoreException {
        String content = null;
        ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
        IPath fullPath = file.getFullPath();
        manager.connect(fullPath, LocationKind.IFILE, null);
        try {
            ITextFileBuffer buffer = manager.getTextFileBuffer(fullPath, LocationKind.IFILE);
            if (buffer != null) {
                content = buffer.getDocument().get();
            }
        } finally {
            manager.disconnect(fullPath, LocationKind.IFILE, null);
        }

        return content;
    }

    /**
     * Reads the content of the java.io.File.
     *
     * @param file the file whose content has to be read
     * @return the content of the file
     * @throws CoreException if the file could not be successfully connected or disconnected
     */
    private static String getFileContent(File file) throws CoreException {
        String content = null;
        ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();

        IPath fullPath = new Path(file.getAbsolutePath());
        manager.connect(fullPath, LocationKind.LOCATION, null);
        try {
            ITextFileBuffer buffer = manager.getTextFileBuffer(fullPath, LocationKind.LOCATION);
            if (buffer != null) {
                content = buffer.getDocument().get();
            }
        } finally {
            manager.disconnect(fullPath, LocationKind.LOCATION, null);
        }
        return content;
    }

}