/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * 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: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.grails.ide.eclipse.editor.gsp.tags; import java.util.Collections; import java.util.LinkedHashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Pattern; import org.codehaus.groovy.ast.FieldNode; import org.codehaus.groovy.ast.Parameter; import org.codehaus.groovy.ast.expr.ClosureExpression; import org.codehaus.groovy.ast.expr.Expression; import org.eclipse.jdt.core.IBuffer; import org.eclipse.jdt.core.IField; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.BodyDeclaration; import org.eclipse.jdt.core.dom.Javadoc; import org.eclipse.jdt.core.dom.TagElement; import org.eclipse.jdt.core.dom.TypeDeclaration; import org.grails.ide.eclipse.core.GrailsCoreActivator; /** * Provides support for parsing GSP tags JavaDoc to * get better content assist and hovers. * * @author Andrew Eisenberg * @since 2.5.2 * @see http://jira.codehaus.org/browse/GRAILS-6593 */ public class GSPTagJavaDocParser { public static class GSPTagDescription { public final String description; public final Map<String, String> attributes; public final Set<String> requiredAttributes; public final boolean isEmpty; public GSPTagDescription(String description, Map<String, String> attributes, Set<String> requiredAttributes, boolean isEmpty) { this.description = description == null ? "" : description; this.attributes = Collections.unmodifiableMap(attributes); this.requiredAttributes = Collections.unmodifiableSet(requiredAttributes); this.isEmpty = isEmpty; } public GSPTagDescription(boolean isEmpty) { this(null, Collections.EMPTY_MAP, Collections.EMPTY_SET, isEmpty); } } private static final String REQUIRED = "REQUIRED"; private static final String KNOWN_ATTRIBUTES = "Known Attributes"; private static final String ATTR = "@attr"; private static final String ENDL = "\n"; private static final String BR = "<br/>"; private static final String STRONG = "<b>"; private static final String STRONG_END = "</b>"; private static final String REQUIRED_STRONG = STRONG + REQUIRED + " " + STRONG_END; private static final String EMPTY_TAG = "@emptyTag"; public static final Pattern OPEN_HEAD_PATTERN = Pattern.compile("<head>", Pattern.CASE_INSENSITIVE); public static final Pattern CLOSE_HEAD_PATTERN = Pattern.compile("</head>", Pattern.CASE_INSENSITIVE); public static final String OPEN_HEAD_REPLACE = "<head>"; public static final String CLOSE_HEAD_REPLACE = "</head>"; public GSPTagDescription parseJavaDoc(IField jdtTagField, FieldNode tagField) { if (jdtTagField == null) { return null; } boolean isEmpty = isProbablyEmpty(tagField); try { ISourceRange javaDocRange = jdtTagField.getJavadocRange(); if (javaDocRange != null) { IBuffer buffer = jdtTagField.getTypeRoot().getBuffer(); String javaDocString = buffer.getText(javaDocRange.getOffset(), javaDocRange.getLength()) + "\nint x;"; char[] javaDocChars = javaDocString.toCharArray(); ASTParser parser = ASTParser.newParser(AST.JLS3); parser.setSource(javaDocChars); parser.setKind(ASTParser.K_CLASS_BODY_DECLARATIONS); TypeDeclaration result = (TypeDeclaration) parser.createAST(null); BodyDeclaration decl = (BodyDeclaration) result.bodyDeclarations().get(0); Javadoc doc = decl.getJavadoc(); @SuppressWarnings("cast") List<ASTNode> tags = (List<ASTNode>) doc.tags(); String description = null; Map<String, String> attrs = new LinkedHashMap<String, String>(); Map<String, String> attrsInDescription = new LinkedHashMap<String, String>(); Set<String> requiredAttrs = new HashSet<String>(); for (ASTNode elt : tags) { switch (elt.getNodeType()) { case ASTNode.TAG_ELEMENT: TagElement tagElt = (TagElement) elt; if (tagElt.getTagName() == null) { if (description == null) { description = fragmentsToText(tagElt); } } else if (tagElt.getTagName().equals(ATTR)) { fragmentsToAttrText(tagElt, attrs, attrsInDescription, requiredAttrs); } else if (tagElt.getTagName().equals(EMPTY_TAG)) { isEmpty = true; } break; } } // now add the attrs and requiredAttrs to the description if (!attrsInDescription.isEmpty()) { StringBuilder sb = new StringBuilder(); if (description != null) { sb.append(description + BR + BR + ENDL); } sb.append(STRONG + KNOWN_ATTRIBUTES + STRONG_END + BR + ENDL); for (String attr : attrsInDescription.values()) { sb.append(attr); } description = sb.toString(); } return new GSPTagDescription(description, attrs, requiredAttrs, isEmpty); } } catch (JavaModelException e) { GrailsCoreActivator.log(e); } catch (IndexOutOfBoundsException e) { GrailsCoreActivator.log(e); } // javadoc is not available // but still keep track of whether or not it should be an empty tag return new GSPTagDescription(isEmpty); } /** * Examine AST structure of the field to see if it has less than 2 parameters to its closure * if so, then it is probably empty * @param tagField * @return */ private boolean isProbablyEmpty(FieldNode tagField) { if (tagField == null) { return false; } Expression initialExpression = tagField.getInitialExpression(); if (initialExpression instanceof ClosureExpression) { Parameter[] parameters = ((ClosureExpression) initialExpression).getParameters(); return parameters == null || parameters.length < 2; } return false; } private void fragmentsToAttrText(TagElement tagElt, Map<String, String> attrs, Map<String, String> attrsInDescription, Set<String> requiredAttrs) { String simpleString = tagElt.toString(); StringTokenizer tokenizer = new StringTokenizer(simpleString, " \t\n\r\f*"); StringBuilder sb = new StringBuilder(); StringBuilder sb2 = new StringBuilder(); if (tokenizer.hasMoreElements()) { // first element is @attr tokenizer.nextElement(); } String attrName = null; if (tokenizer.hasMoreElements()) { attrName = tokenizer.nextToken(); sb.append(STRONG + attrName + STRONG_END); sb2.append(STRONG + attrName + STRONG_END); } boolean isRequired; if (tokenizer.hasMoreElements()) { String maybeRequired = tokenizer.nextToken(); isRequired = maybeRequired.compareToIgnoreCase(REQUIRED) == 0; sb2.append(" : "); if (isRequired) { sb.append(BR + ENDL + REQUIRED_STRONG); sb2.append(REQUIRED_STRONG); } sb.append(BR + BR + ENDL); if (!isRequired) { sb.append(maybeRequired); sb2.append(maybeRequired); } } else { isRequired = false; } while (tokenizer.hasMoreElements()) { if (sb.charAt(sb.length()-1) != ' ' && sb.charAt(sb.length()-1) != '\n') { sb.append(" "); sb2.append(" "); } String token = tokenizer.nextToken(); sb.append(token); sb2.append(token); } sb2.append(BR + ENDL); if (attrName != null) { attrs.put(attrName, sb.toString()); attrsInDescription.put(attrName, sb2.toString()); if (isRequired) { requiredAttrs.add(attrName); } } } protected String fragmentsToText(TagElement tagElt) { String simpleString = tagElt.toString(); StringTokenizer tokenizer = new StringTokenizer(simpleString, " \t\n\r\f*"); StringBuilder sb = new StringBuilder(); while (tokenizer.hasMoreElements()) { String token = tokenizer.nextToken(); if (token.length() > 0) { // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=367378 // can't have a head tag in the javadoc token = OPEN_HEAD_PATTERN.matcher(token).replaceFirst(OPEN_HEAD_REPLACE); token = CLOSE_HEAD_PATTERN.matcher(token).replaceFirst(CLOSE_HEAD_REPLACE); sb.append(token); if (tokenizer.hasMoreElements()) { sb.append(" "); } } } return sb.toString(); } }