content: /*******************************************************************************
   * Copyright (c) 2000, 2013 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
   *******************************************************************************/
  package org.eclipse.wst.jsdt.internal.ui.text.correction;
  
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Set;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.core.runtime.CoreException;
  import org.eclipse.core.runtime.IStatus;
  import org.eclipse.jface.text.BadLocationException;
  import org.eclipse.jface.text.IDocument;
  import org.eclipse.jface.text.IRegion;
  import org.eclipse.jface.text.TextUtilities;
  import org.eclipse.swt.graphics.Image;
  import org.eclipse.text.edits.InsertEdit;
  import org.eclipse.text.edits.TextEdit;
  import org.eclipse.text.edits.TextEditGroup;
  import org.eclipse.ui.ISharedImages;
  import org.eclipse.wst.jsdt.core.IJavaScriptProject;
  import org.eclipse.wst.jsdt.core.IJavaScriptUnit;
  import org.eclipse.wst.jsdt.core.dom.AST;
  import org.eclipse.wst.jsdt.core.dom.ASTNode;
  import org.eclipse.wst.jsdt.core.dom.AbstractTypeDeclaration;
  import org.eclipse.wst.jsdt.core.dom.BodyDeclaration;
  import org.eclipse.wst.jsdt.core.dom.FieldDeclaration;
  import org.eclipse.wst.jsdt.core.dom.FunctionDeclaration;
  import org.eclipse.wst.jsdt.core.dom.IFunctionBinding;
  import org.eclipse.wst.jsdt.core.dom.ITypeBinding;
  import org.eclipse.wst.jsdt.core.dom.JSdoc;
  import org.eclipse.wst.jsdt.core.dom.Name;
  import org.eclipse.wst.jsdt.core.dom.PrimitiveType;
  import org.eclipse.wst.jsdt.core.dom.SimpleName;
  import org.eclipse.wst.jsdt.core.dom.SingleVariableDeclaration;
  import org.eclipse.wst.jsdt.core.dom.StructuralPropertyDescriptor;
  import org.eclipse.wst.jsdt.core.dom.TagElement;
  import org.eclipse.wst.jsdt.core.dom.TextElement;
  import org.eclipse.wst.jsdt.core.dom.Type;
  import org.eclipse.wst.jsdt.core.dom.TypeDeclaration;
  import org.eclipse.wst.jsdt.core.dom.VariableDeclaration;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ASTRewrite;
  import org.eclipse.wst.jsdt.core.dom.rewrite.ListRewrite;
  import org.eclipse.wst.jsdt.internal.corext.dom.ASTNodes;
  import org.eclipse.wst.jsdt.internal.corext.dom.Bindings;
  import org.eclipse.wst.jsdt.internal.corext.util.Strings;
  import org.eclipse.wst.jsdt.internal.ui.JavaPluginImages;
  import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin;
  import org.eclipse.wst.jsdt.internal.ui.JavaUIStatus;
  import org.eclipse.wst.jsdt.ui.CodeGeneration;
  import org.eclipse.wst.jsdt.ui.text.java.IInvocationContext;
  import org.eclipse.wst.jsdt.ui.text.java.IProblemLocation;
  
  /**
   *
   */
  public class JavadocTagsSubProcessor {
  
  	private static final class AddJavadocCommentProposal extends CUCorrectionProposal {
  
  	 	private final int fInsertPosition;
  		private final String fComment;
  
  		private AddJavadocCommentProposal(String name, IJavaScriptUnit cu, int relevance, int insertPosition, String comment) {
  			super(name, cu, relevance, JavaPluginImages.get(JavaPluginImages.IMG_OBJS_JAVADOCTAG));
  			fInsertPosition= insertPosition;
  			fComment= comment;
  		}
  
  		protected void addEdits(IDocument document, TextEdit rootEdit) throws CoreException {
  			try {
  				String lineDelimiter= TextUtilities.getDefaultLineDelimiter(document);
  				final IJavaScriptProject project= getCompilationUnit().getJavaScriptProject();
  				IRegion region= document.getLineInformationOfOffset(fInsertPosition);
  
  				String lineContent= document.get(region.getOffset(), region.getLength());
  				String indentString= Strings.getIndentString(lineContent, project);
  				String str= Strings.changeIndent(fComment, 0, project, indentString, lineDelimiter);
  				InsertEdit edit= new InsertEdit(fInsertPosition, str);
  				rootEdit.addChild(edit);
  				if (fComment.charAt(fComment.length() - 1) != '\n') {
  					rootEdit.addChild(new InsertEdit(fInsertPosition, lineDelimiter));
  					rootEdit.addChild(new InsertEdit(fInsertPosition, indentString));
  				}
  			} catch (BadLocationException e) {
  				throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, e));
  			}
  		}
  	}
  
  	private static final class AddMissingJavadocTagProposal extends LinkedCorrectionProposal {
  
  		private final BodyDeclaration fBodyDecl; // MethodDecl or TypeDecl
  		private final ASTNode fMissingNode;
  
  		public AddMissingJavadocTagProposal(String label, IJavaScriptUnit cu, BodyDeclaration methodDecl, ASTNode missingNode, int relevance) {
  			super(label, cu, null, relevance, JavaPluginImages.get(JavaPluginImages.IMG_OBJS_JAVADOCTAG));
  			fBodyDecl= methodDecl;
  			fMissingNode= missingNode;
  		}
  
  		protected ASTRewrite getRewrite() throws CoreException {
  			AST ast= fBodyDecl.getAST();
  			ASTRewrite rewrite= ASTRewrite.create(ast);
  		 	insertMissingJavadocTag(rewrite, fMissingNode, fBodyDecl);
  			return rewrite;
  		}
  
  		private void insertMissingJavadocTag(ASTRewrite rewrite, ASTNode missingNode, BodyDeclaration bodyDecl) {
  			AST ast= bodyDecl.getAST();
  			JSdoc javadoc= bodyDecl.getJavadoc();
  		 	ListRewrite tagsRewriter= rewrite.getListRewrite(javadoc, JSdoc.TAGS_PROPERTY);
  
  		 	StructuralPropertyDescriptor location= missingNode.getLocationInParent();
  		 	TagElement newTag;
  		 	if (location == SingleVariableDeclaration.NAME_PROPERTY) {
  		 		// normal parameter
  		 		SingleVariableDeclaration decl= (SingleVariableDeclaration) missingNode.getParent();
  
  				String name= ((SimpleName) missingNode).getIdentifier();
  				newTag= ast.newTagElement();
  				newTag.setTagName(TagElement.TAG_PARAM);
  				List fragments= newTag.fragments();
  				fragments.add(ast.newSimpleName(name));
  
  				FunctionDeclaration methodDeclaration= (FunctionDeclaration) bodyDecl;
  				List params= methodDeclaration.parameters();
  
  				Set sameKindLeadingNames= getPreviousParamNames(params, decl);
  
  				insertTag(tagsRewriter, newTag, sameKindLeadingNames);
  		 	} else if (location == FunctionDeclaration.RETURN_TYPE2_PROPERTY) {
  				newTag= ast.newTagElement();
  				newTag.setTagName(TagElement.TAG_RETURN);
  				insertTag(tagsRewriter, newTag, null);
  		 	} else if (location == FunctionDeclaration.THROWN_EXCEPTIONS_PROPERTY) {
  				newTag= ast.newTagElement();
  				newTag.setTagName(TagElement.TAG_THROWS);
  				TextElement excNode= ast.newTextElement();
  				excNode.setText(ASTNodes.asString(missingNode));
  				newTag.fragments().add(excNode);
  				List exceptions= ((FunctionDeclaration) bodyDecl).thrownExceptions();
  				insertTag(tagsRewriter, newTag, getPreviousExceptionNames(exceptions, missingNode));
  		 	} else {
  		 		Assert.isTrue(false, "AddMissingJavadocTagProposal: unexpected node location"); //$NON-NLS-1$
  		 		return;
  		 	}
  
  			TextElement textElement= ast.newTextElement();
  			textElement.setText(""); //$NON-NLS-1$
  			newTag.fragments().add(textElement);
  			addLinkedPosition(rewrite.track(textElement), false, "comment_start"); //$NON-NLS-1$
  		}
  	}
  
  	private static final class AddAllMissingJavadocTagsProposal extends LinkedCorrectionProposal {
  
  		private final BodyDeclaration fBodyDecl;
  
  		public AddAllMissingJavadocTagsProposal(String label, IJavaScriptUnit cu, BodyDeclaration bodyDecl, int relevance) {
  			super(label, cu, null, relevance, JavaPluginImages.get(JavaPluginImages.IMG_OBJS_JAVADOCTAG));
  			fBodyDecl= bodyDecl;
  		}
  
  		protected ASTRewrite getRewrite() throws CoreException {
  			ASTRewrite rewrite= ASTRewrite.create(fBodyDecl.getAST());
  			if (fBodyDecl instanceof FunctionDeclaration) {
  				insertAllMissingMethodTags(rewrite, (FunctionDeclaration) fBodyDecl);
  			} else {
  				insertAllMissingTypeTags(rewrite, (TypeDeclaration) fBodyDecl);
  			}
  			return rewrite;
  		}
  
  		private void insertAllMissingMethodTags(ASTRewrite rewriter, FunctionDeclaration methodDecl) {
  		 	AST ast= methodDecl.getAST();
  		 	JSdoc javadoc= methodDecl.getJavadoc();
  		 	ListRewrite tagsRewriter= rewriter.getListRewrite(javadoc, JSdoc.TAGS_PROPERTY);
  
  		 	List typeParamNames= new ArrayList();
  		 	List params= methodDecl.parameters();
  		 	for (int i= params.size() - 1; i >= 0 ; i--) {
  		 		SingleVariableDeclaration decl= (SingleVariableDeclaration) params.get(i);
  		 		String name= decl.getName().getIdentifier();
  		 		if (findTag(javadoc, TagElement.TAG_PARAM, name) == null) {
  		 			TagElement newTag= ast.newTagElement();
  		 			newTag.setTagName(TagElement.TAG_PARAM);
  		 			newTag.fragments().add(ast.newSimpleName(name));
  					insertTabStop(rewriter, newTag.fragments(), "methParam" + i); //$NON-NLS-1$
  		 			Set sameKindLeadingNames= getPreviousParamNames(params, decl);
  		 			sameKindLeadingNames.addAll(typeParamNames);
  		 			insertTag(tagsRewriter, newTag, sameKindLeadingNames);
  		 		}
  		 	}
  		 	if (!methodDecl.isConstructor()) {
  		 		Type type= methodDecl.getReturnType2();
  		 		if (type != null && (!type.isPrimitiveType() || (((PrimitiveType) type).getPrimitiveTypeCode() != PrimitiveType.VOID))) {
  		 			if (findTag(javadoc, TagElement.TAG_RETURN, null) == null) {
  		 				TagElement newTag= ast.newTagElement();
  		 				newTag.setTagName(TagElement.TAG_RETURN);
  						insertTabStop(rewriter, newTag.fragments(), "return"); //$NON-NLS-1$
  		 				insertTag(tagsRewriter, newTag, null);
  		 			}
  		 		}
  		 	}
  		 	List thrownExceptions= methodDecl.thrownExceptions();
  		 	for (int i= thrownExceptions.size() - 1; i >= 0 ; i--) {
  		 		Name exception= (Name) thrownExceptions.get(i);
  		 		ITypeBinding binding= exception.resolveTypeBinding();
  		 		if (binding != null) {
  		 			String name= binding.getName();
  		 			if (findThrowsTag(javadoc, name) == null) {
  		 				TagElement newTag= ast.newTagElement();
  		 				newTag.setTagName(TagElement.TAG_THROWS);
  						TextElement excNode= ast.newTextElement();
  						excNode.setText(ASTNodes.asString(exception));
  		 				newTag.fragments().add(excNode);
  						insertTabStop(rewriter, newTag.fragments(), "exception" + i); //$NON-NLS-1$
  		 				insertTag(tagsRewriter, newTag, getPreviousExceptionNames(thrownExceptions, exception));
  		 			}
  		 		}
  		 	}
  		 }
  
  		private void insertAllMissingTypeTags(ASTRewrite rewriter, TypeDeclaration typeDecl) {
  			AST ast= typeDecl.getAST();
  			JSdoc javadoc= typeDecl.getJavadoc();
  			ListRewrite tagsRewriter= rewriter.getListRewrite(javadoc, JSdoc.TAGS_PROPERTY);
  		}
  
  		private void insertTabStop(ASTRewrite rewriter, List fragments, String linkedName) {
  			TextElement textElement= rewriter.getAST().newTextElement();
  			textElement.setText(""); //$NON-NLS-1$
  			fragments.add(textElement);
  			addLinkedPosition(rewriter.track(textElement), false, linkedName);
  		}
  
  	}
  
  	public static void getMissingJavadocTagProposals(IInvocationContext context, IProblemLocation problem, Collection proposals) {
  	 	ASTNode node= problem.getCoveringNode(context.getASTRoot());
  	 	if (node == null) {
  	 		return;
  	 	}
  	 	node= ASTNodes.getNormalizedNode(node);
  
  	 	BodyDeclaration bodyDeclaration= ASTResolving.findParentBodyDeclaration(node);
  	 	if (bodyDeclaration == null) {
  	 		return;
  	 	}
  	 	JSdoc javadoc= bodyDeclaration.getJavadoc();
  	 	if (javadoc == null) {
  	 		return;
  	 	}
  
  	 	String label;
  	 	StructuralPropertyDescriptor location= node.getLocationInParent();
  	 	if (location == SingleVariableDeclaration.NAME_PROPERTY) {
  	 		label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_paramtag_description;
  	 		if (node.getParent().getLocationInParent() != FunctionDeclaration.PARAMETERS_PROPERTY) {
  	 			return; // paranoia checks
  	 		}
  	 	} else if (location == FunctionDeclaration.RETURN_TYPE2_PROPERTY) {
  	 		label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_returntag_description;
  	 	} else if (location == FunctionDeclaration.THROWN_EXCEPTIONS_PROPERTY) {
  	 		label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_throwstag_description;
  	 	} else {
  	 		return;
  	 	}
  	 	ASTRewriteCorrectionProposal proposal= new AddMissingJavadocTagProposal(label, context.getCompilationUnit(), bodyDeclaration, node, 1); 
  	 	proposals.add(proposal);
  
  	 	String label2= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_allmissing_description;
  	 	ASTRewriteCorrectionProposal addAllMissing= new AddAllMissingJavadocTagsProposal(label2, context.getCompilationUnit(), bodyDeclaration, 5); 
  	 	proposals.add(addAllMissing);
  	}
  
  	public static void getMissingJavadocCommentProposals(IInvocationContext context, IProblemLocation problem, Collection proposals) throws CoreException {
  		ASTNode node= problem.getCoveringNode(context.getASTRoot());
  		if (node == null) {
  			return;
  		}
  		BodyDeclaration declaration= ASTResolving.findParentBodyDeclaration(node);
  		if (declaration == null) {
  			return;
  		}
  		IJavaScriptUnit cu= context.getCompilationUnit();
  		ITypeBinding binding= Bindings.getBindingOfParentType(declaration);
  		if (binding == null) {
  			return;
  		}
  
  		if (declaration instanceof FunctionDeclaration) {
  			FunctionDeclaration methodDecl= (FunctionDeclaration) declaration;
  			IFunctionBinding methodBinding= methodDecl.resolveBinding();
  			IFunctionBinding overridden= null;
  			if (methodBinding != null) {
  				overridden= Bindings.findOverriddenMethod(methodBinding, true);
  			}
  
  			String string= CodeGeneration.getMethodComment(cu, binding.getName(), methodDecl, overridden, String.valueOf('\n'));
  			if (string != null) {
  				String label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_method_description;
  				proposals.add(new AddJavadocCommentProposal(label, cu, 1, declaration.getStartPosition(), string));
  			}
  		} else if (declaration instanceof AbstractTypeDeclaration) {
  			String typeQualifiedName= Bindings.getTypeQualifiedName(binding);
  			
  			String string= CodeGeneration.getTypeComment(cu, typeQualifiedName, String.valueOf('\n'));
  			if (string != null) {
  				String label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_type_description;
  				proposals.add(new AddJavadocCommentProposal(label, cu, 1, declaration.getStartPosition(), string));
  			}
  		} else if (declaration instanceof FieldDeclaration) {
  			String comment= "/**\n *\n */\n"; //$NON-NLS-1$
  			List fragments= ((FieldDeclaration)declaration).fragments();
  			if (fragments != null && fragments.size() > 0) {
  				VariableDeclaration decl= (VariableDeclaration)fragments.get(0);
  				String fieldName= decl.getName().getIdentifier();
  				String typeName= binding.getName();
  				comment= CodeGeneration.getFieldComment(cu, typeName, fieldName, String.valueOf('\n'));
  			}
  			if (comment != null) {
  				String label= CorrectionMessages.JavadocTagsSubProcessor_addjavadoc_field_description;
  				proposals.add(new AddJavadocCommentProposal(label, cu, 1, declaration.getStartPosition(), comment));
  			}
  		}
  	}
  
  	private static Set getPreviousParamNames(List params, ASTNode missingNode) {
  		Set previousNames=  new HashSet();
  		for (int i = 0; i < params.size(); i++) {
  			SingleVariableDeclaration curr= (SingleVariableDeclaration) params.get(i);
  			if (curr == missingNode) {
  				return previousNames;
  			}
  			previousNames.add(curr.getName().getIdentifier());
  		}
  		return previousNames;
  	}
  
  	private static Set getPreviousExceptionNames(List list, ASTNode missingNode) {
  		Set previousNames=  new HashSet();
  		for (int i= 0; i < list.size() && missingNode != list.get(i); i++) {
  			Name curr= (Name) list.get(i);
  			previousNames.add(ASTNodes.getSimpleNameIdentifier(curr));
  		}
  		return previousNames;
  	}
  
  	public static TagElement findTag(JSdoc javadoc, String name, String arg) {
  		List tags= javadoc.tags();
  		int nTags= tags.size();
  		for (int i= 0; i < nTags; i++) {
  			TagElement curr= (TagElement) tags.get(i);
  			if (name.equals(curr.getTagName())) {
  				if (arg != null) {
  					String argument= getArgument(curr);
  					if (arg.equals(argument)) {
  						return curr;
  					}
  				} else {
  					return curr;
  				}
  			}
  		}
  		return null;
  	}
  
  	public static TagElement findParamTag(JSdoc javadoc, String arg) {
  		List tags= javadoc.tags();
  		int nTags= tags.size();
  		for (int i= 0; i < nTags; i++) {
  			TagElement curr= (TagElement) tags.get(i);
  			String currName= curr.getTagName();
  			if (TagElement.TAG_PARAM.equals(currName)) {
  				String argument= getArgument(curr);
  				if (arg.equals(argument)) {
  					return curr;
  				}
  			}
  		}
  		return null;
  	}
  
  
  	public static TagElement findThrowsTag(JSdoc javadoc, String arg) {
  		List tags= javadoc.tags();
  		int nTags= tags.size();
  		for (int i= 0; i < nTags; i++) {
  			TagElement curr= (TagElement) tags.get(i);
  			String currName= curr.getTagName();
  			if (TagElement.TAG_THROWS.equals(currName) || TagElement.TAG_EXCEPTION.equals(currName)) {  
  				String argument= getArgument(curr);
  				if (arg.equals(argument)) {
  					return curr;
  				}
  			}
  		}
  		return null;
  	}
  
  	public static void insertTag(ListRewrite rewriter, TagElement newElement, Set sameKindLeadingNames) {
  		insertTag(rewriter, newElement, sameKindLeadingNames, null);
  	}
  
  	public static void insertTag(ListRewrite rewriter, TagElement newElement, Set sameKindLeadingNames, TextEditGroup groupDescription) {
  		List tags= rewriter.getRewrittenList();
  
  		String insertedTagName= newElement.getTagName();
  
  		ASTNode after= null;
  		int tagRanking= getTagRanking(insertedTagName);
  		for (int i= tags.size() - 1; i >= 0; i--) {
  			TagElement curr= (TagElement) tags.get(i);
  			String tagName= curr.getTagName();
  			if (tagName == null || tagRanking > getTagRanking(tagName)) {
  				after= curr;
  				break;
  			}
  			if (sameKindLeadingNames != null && isSameTag(insertedTagName, tagName)) {
  				String arg= getArgument(curr);
  				if (arg != null && sameKindLeadingNames.contains(arg)) {
  					after= curr;
  					break;
  				}
  			}
  		}
  		if (after != null) {
  			rewriter.insertAfter(newElement, after, groupDescription);
  		} else {
  			rewriter.insertFirst(newElement, groupDescription);
  		}
  	}
  
  	private static boolean isSameTag(String insertedTagName, String tagName) {
  		if (insertedTagName.equals(tagName)) {
  			return true;
  		}
  		if (TagElement.TAG_EXCEPTION.equals(tagName)) {
  			return TagElement.TAG_THROWS.equals(insertedTagName);
  		}
  		return false;
  	}
  	
  	private static String[] TAG_ORDER= { // see http://java.sun.com/j2se/javadoc/writingdoccomments/index.html#orderoftags
  		TagElement.TAG_AUTHOR,
  		TagElement.TAG_VERSION,
  		TagElement.TAG_PARAM,
  		TagElement.TAG_RETURN,
  		TagElement.TAG_THROWS, // synonym to TAG_EXCEPTION
  		TagElement.TAG_SEE,
  		TagElement.TAG_SINCE,
  		TagElement.TAG_SERIAL,
  		TagElement.TAG_DEPRECATED
  	};
  	
  	private static int getTagRanking(String tagName) {
  		if (tagName.equals(TagElement.TAG_EXCEPTION)) {
  			tagName= TagElement.TAG_THROWS;
  		}
  		for (int i= 0; i < TAG_ORDER.length; i++) {
  			if (tagName.equals(TAG_ORDER[i])) {
  				return i;
  			}
  		}
  		return TAG_ORDER.length;
  	}
  
  	private static String getArgument(TagElement curr) {
  		List fragments= curr.fragments();
  		if (!fragments.isEmpty()) {
  			Object first= fragments.get(0);
  			if (first instanceof Name) {
  				return ASTNodes.getSimpleNameIdentifier((Name) first);
  			} else if (first instanceof TextElement && TagElement.TAG_PARAM.equals(curr.getTagName())) {
  				String text= ((TextElement) first).getText();
  				if ("<".equals(text) && fragments.size() >= 3) { //$NON-NLS-1$
  					Object second= fragments.get(1);
  					Object third= fragments.get(2);
  					if (second instanceof Name && third instanceof TextElement && ">".equals(((TextElement) third).getText())) { //$NON-NLS-1$
  						return '<' + ASTNodes.getSimpleNameIdentifier((Name) second) + '>';
  					}
  				} else if (text.startsWith(String.valueOf('<')) && text.endsWith(String.valueOf('>')) && text.length() > 2) {
  					return text.substring(1, text.length() - 1);
  				}
  			}
  		}
  		return null;
  	}
  
  	public static void getRemoveJavadocTagProposals(IInvocationContext context, IProblemLocation problem, Collection proposals) {
  		ASTNode node= problem.getCoveringNode(context.getASTRoot());
  		while (node != null && !(node instanceof TagElement)) {
  			node= node.getParent();
  		}
  		if (node == null) {
  			return;
  		}
  		ASTRewrite rewrite= ASTRewrite.create(node.getAST());
  		rewrite.remove(node, null);
  
  		String label= CorrectionMessages.JavadocTagsSubProcessor_removetag_description;
  		Image image= JavaScriptPlugin.getDefault().getWorkbench().getSharedImages().getImage(ISharedImages.IMG_TOOL_DELETE);
  		proposals.add(new ASTRewriteCorrectionProposal(label, context.getCompilationUnit(), rewrite, 5, image)); 
  	}
  }
  