/*
* Copyright 2012 Amazon Technologies, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://aws.amazon.com/apache2.0
*
* This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
* OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and
* limitations under the License.
*/
package com.amazonaws.eclipse.cloudformation.templates.editor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.contentassist.CompletionProposal;
import org.eclipse.jface.text.contentassist.ContextInformation;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
import org.eclipse.ui.statushandlers.StatusManager;
import com.amazonaws.eclipse.cloudformation.CloudFormationPlugin;
import com.amazonaws.eclipse.cloudformation.templates.TemplateNode;
import com.amazonaws.eclipse.cloudformation.templates.TemplateObjectNode;
import com.amazonaws.eclipse.cloudformation.templates.TemplateValueNode;
import com.amazonaws.eclipse.cloudformation.templates.editor.TemplateEditor.TemplateDocument;
import com.amazonaws.eclipse.cloudformation.templates.schema.IntrinsicFunction;
import com.amazonaws.eclipse.cloudformation.templates.schema.PseudoParameter;
import com.amazonaws.eclipse.cloudformation.templates.schema.Schema;
import com.amazonaws.eclipse.cloudformation.templates.schema.SchemaProperty;
import com.amazonaws.eclipse.cloudformation.templates.schema.TemplateSchemaRules;
public class TemplateContentAssistProcessor implements IContentAssistProcessor {
/** The parsed schema rules for the Amazon CloudFormation template. */
private static final TemplateSchemaRules schemaRules = TemplateSchemaRules
.getInstance();
/** The path from the root to where the Resource JSON objects are present. */
private static final String resourcesPath = "ROOT/Resources/";
/**
* The name of the field specifying the type of the Resource in the JSON
* object.
*/
private static final String RESOURCE_TYPE = "Type";
/** The name of the Json Object specifying the list of resources. */
private static final String RESOURCES = "Resources";
/** The root object of the Json document. */
private static final String ROOT = "ROOT/";
private static final String EMPTY_STRING = "";
/**
* Identifies the node in the document for the given path and returns it.
*
* @param document
* The document associated with the editor.
* @param path
* The path from the Root to the node.
*/
private TemplateNode lookupNodeByPath(TemplateDocument document, String path) {
TemplateNode node = ((TemplateDocument) document).getModel();
if (path.startsWith("ROOT"))
path = path.substring("ROOT".length());
else
throw new RuntimeException("Unexpected path encountered");
StringTokenizer tokenizer = new StringTokenizer(path, "/");
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (node != null && node.isObject()) {
TemplateObjectNode object = (TemplateObjectNode) node;
node = object.get(token);
} else {
throw new RuntimeException("Unexpected node structure");
}
}
return node;
}
/**
* Generates a proposal for the given field.
*
* @param templateDocument
* The document associated with the editor.
* @param offset
* The current cursor position.
* @param fieldName
* The name of the field.
* @param schemaProperty
* The schema property associated with the field.
*/
private ICompletionProposal newFieldCompletionProposal(
TemplateDocument templateDocument, int offset, String fieldName,
SchemaProperty schemaProperty, String stringToReplace) {
char previousChar = ' ';
try {
previousChar = templateDocument.getChar(offset - 1);
} catch (BadLocationException e) {
}
boolean needsQuotes = previousChar != '"';
String insertionText = fieldName;
if (needsQuotes){
insertionText = "\"" + fieldName;
stringToReplace = "\"" + stringToReplace;
}
insertionText += "\" : ";
int finalCursorPosition = -1;
if (schemaProperty.getType().equalsIgnoreCase("string")) {
insertionText += "\"\"";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("array")) {
insertionText += "[]";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("named-array")) {
insertionText += "{}";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("number")) {
insertionText += "\"\"";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("object")) {
insertionText += "{}";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("resource")) {
insertionText += "{}";
finalCursorPosition = insertionText.length() - 1;
} else if (schemaProperty.getType().equalsIgnoreCase("json")) {
insertionText += "{}";
finalCursorPosition = insertionText.length() - 1;
} else {
IStatus status = new Status(IStatus.INFO,
CloudFormationPlugin.PLUGIN_ID, "Unhandled property type: "
+ schemaProperty.getType());
StatusManager.getManager().handle(status, StatusManager.LOG);
}
char nextChar = DocumentUtils.readToNextChar(templateDocument, offset);
if (nextChar != '}' && nextChar != ',' && nextChar != ']')
insertionText += ", ";
if (finalCursorPosition == -1)
finalCursorPosition = insertionText.length();
CFCompletionProposal proposal = newCompletionProposal(offset,
fieldName, insertionText, finalCursorPosition,
schemaProperty.getDescription(),stringToReplace);
// if previousChar is a type of END_TOKEN
previousChar = DocumentUtils.readToPreviousChar(templateDocument,
offset - 1);
if (previousChar == '"' || previousChar == '}' || previousChar == ']') {
proposal.setAdditionalCommaPosition(DocumentUtils
.findPreviousCharPosition(templateDocument, offset,
previousChar) + 1);
}
return proposal;
}
/**
* Creates a new completion proposal with the given parameters.
*
* @param offset
* The current cursor position.
* @param label
* The label to displayed in the the proposal.
* @param insertionText
* The text to be inserted when a proposal is selected.
* @param finalCursorPosition
* The final cursor position after inserting the proposal.
* @param description
* The description for the proposal to be displayed in the tool
* tip.
* @param stringToReplace The string to be replaced when a proposal is selected.
*/
private CFCompletionProposal newCompletionProposal(int offset,
String label, String insertionText, int finalCursorPosition,
String description,String stringToReplace) {
IContextInformation contextInfo = new ContextInformation(label, label);
return new CFCompletionProposal(insertionText, offset-stringToReplace.length(), stringToReplace.length(),
finalCursorPosition, null, label, contextInfo, description);
}
/**
* Provides the auto completions for all the root level objects.
*
* @param offset
* The current cursor position
* @param document
* The document associated with the editor.
* @param path
* Path from the root of the JSON object to the current cursor
* position.
* @param proposals
* List containing the proposals.
*/
private void provideRootLevelAutoCompletions(int offset,
TemplateDocument document, String path,
List<ICompletionProposal> proposals) {
if (!(path.startsWith(ROOT))) {
throw new RuntimeException("Unexpected path encountered");
}
TemplateSchemaRules schemaRules = TemplateSchemaRules.getInstance();
String currentPath = "ROOT";
if (path.startsWith("ROOT"))
path = path.substring("ROOT".length());
else
throw new RuntimeException("Unexpected path encountered");
// TODO: This is all code just to find the schema for the current
// position
StringTokenizer tokenizer = new StringTokenizer(path, "/");
String stringToReplace = DocumentUtils.readToPreviousQuote(document, offset);
Schema schema = schemaRules.getTopLevelSchema();
SchemaProperty lastSchemaProperty = null;
boolean isSchemaLookupProperty = false;
boolean isDefaultChildSchema = false;
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
currentPath += "/" + token;
SchemaProperty schemaProperty = null;
if (schema != null) {
schemaProperty = schema.getProperty(token);
}
if (schemaProperty == null && isDefaultChildSchema == false
&& isSchemaLookupProperty == false) {
// We don't know anything about this node
return;
}
if (isSchemaLookupProperty) {
String schemaLookupProperty = lastSchemaProperty
.getSchemaLookupProperty();
TemplateNode node = lookupNodeByPath(document, currentPath);
if (node.isObject()) {
String lookupValue = null;
TemplateObjectNode object = (TemplateObjectNode) node;
TemplateNode fieldValue = object.get(schemaLookupProperty);
if (fieldValue.isValue()) {
TemplateValueNode value = (TemplateValueNode) fieldValue;
lookupValue = value.getText();
schema = lastSchemaProperty.getChildSchema(lookupValue);
}
}
isSchemaLookupProperty = false;
} else if (isDefaultChildSchema) {
schema = lastSchemaProperty.getDefaultChildSchema();
isDefaultChildSchema = false;
} else if (schemaProperty.getSchemaLookupProperty() != null) {
isSchemaLookupProperty = true;
schema = null;
} else if (schemaProperty.getDefaultChildSchema() != null) {
isDefaultChildSchema = true;
schema = null;
} else if (schemaProperty.getSchema() != null) {
schema = schemaProperty.getSchema();
} else {
schema = null;
}
lastSchemaProperty = schemaProperty;
}
if (schema != null) {
ArrayList<String> properties = new ArrayList<String>(
schema.getProperties());
Collections.sort(properties);
Set<String> existingFields = new HashSet<String>();
TemplateNode node = lookupNodeByPath(document, "ROOT" + path);
if (node.isObject()) {
TemplateObjectNode objectNode = (TemplateObjectNode) node;
for (Entry<String, TemplateNode> entry : objectNode.getFields()) {
existingFields.add(entry.getKey());
}
}
for (String field : properties) {
// Don't show completions for fields already present in the
// document
if (existingFields.contains(field))
continue;
proposals.add(newFieldCompletionProposal(document, offset,
field, schema.getProperty(field),stringToReplace));
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#
* computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)
*/
public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer,
int offset) {
List<ICompletionProposal> proposals = new ArrayList<ICompletionProposal>();
TemplateDocument document = (TemplateDocument) viewer.getDocument();
StringBuilder path = new StringBuilder();
List<String> subPaths = document.getPath();
if (subPaths != null && !subPaths.isEmpty()) {
for (String subPath : subPaths) {
path.append(subPath + "/");
}
} else {
TemplateNode node = document.findNode(offset);
path.append(node.getPath());
}
char previousChar = DocumentUtils.readToPreviousChar(document,
offset - 1);
char unmatchedOpenBrace = DocumentUtils
.readToPreviousUnmatchedOpenBrace(document, offset - 1);
if (unmatchedOpenBrace == '{') {
if (path.toString().startsWith(resourcesPath)) {
provideResourceTypes(offset, document, path.toString(),
proposals);
} else if (path.toString().startsWith(ROOT)) {
provideRootLevelAutoCompletions(offset, document,
path.toString(), proposals);
}
} else if (unmatchedOpenBrace == '[' || previousChar == ':') {
// TODO: Completions for known values for a field
// TODO: Intrinsic Functions should include "{}"
addIntrinsicFunctionCompletions(offset, proposals);
addPseudoParameterCompletions(offset, proposals);
}
return proposals.toArray(new ICompletionProposal[proposals.size()]);
}
/**
* Creates a new auto completion proposal for the given field.
*
* @param offset
* The current position of the cursor.
* @param autoCompleteField
* The value that is to be shown in the auto complete drop box.
* @param insertionText
* The actual value to be inserted when an option is selected.
* @param fieldDescription
* The description for the auto complete option to be shown in
* tool tip.
* @param stringToReplace
* The string to replace in the editor.
* @return A completion proposal with all the options added.
*/
private ICompletionProposal createCompletionProposal(int offset,
String autoCompleteField, String insertionText,
String fieldDescription, String stringToReplace) {
return new CFCompletionProposal(insertionText, offset
- stringToReplace.length(), stringToReplace.length(),
insertionText.length(), null, autoCompleteField,
new ContextInformation(autoCompleteField, fieldDescription),
fieldDescription);
}
/**
* Identifies the set of resources to be shown for auto completion.
*
* @param offset
* The current cursor position.
* @param document
* The document associated with the editor.
* @param path
* Path from the root of the JSON object to the current cursor
* position.
* @param proposals
* List of auto completion proposals.
*/
private void provideResourceTypes(int offset, TemplateDocument document,
String path, List<ICompletionProposal> proposals) {
if (!(path.startsWith(resourcesPath))) {
throw new RuntimeException("Unexpected path encountered");
}
String stringToReplace = DocumentUtils.readToPreviousQuote(document,
offset);
char nextChar = DocumentUtils.readToNextChar(document, offset);
boolean needsQuotes = nextChar != '"';
path = path.substring(resourcesPath.length());
Set<String> resourceTypes = schemaRules.getResourceTypeNames();
StringTokenizer tokenizer = new StringTokenizer(path, "/");
String nextToken;
if (tokenizer.countTokens() == 2) {
// Skipping the resource name
tokenizer.nextToken();
// Fetching the next Token.
nextToken = tokenizer.nextToken();
if (nextToken.equals(RESOURCE_TYPE)) {
addListToProposals(offset,
new ArrayList<String>(resourceTypes), proposals,
stringToReplace, needsQuotes, null);
}
}
}
/**
* Creates a completion proposal for the given set and adds it to the list.
*
* @param offset
* The current cursor position.
* @param values
* The values for which proposals are to be created.
* @param proposals
* The list that contains the proposals.
* @param stringToReplace
* The string to be replaced when a proposal is selected.
* @param needsQuotes
* A boolean value indicating if a double quote needs to be added
* while insertion.
*/
private void addListToProposals(int offset, List<String> values,
List<ICompletionProposal> proposals, String stringToReplace,
boolean needsQuotes, List<String> existingFields) {
Collections.sort(values);
SchemaProperty schemaProperty = schemaRules.getTopLevelSchema()
.getProperty(RESOURCES);
Schema childSchema;
String insertionText;
for (String value : values) {
if (value.startsWith(stringToReplace)) {
insertionText = value;
childSchema = schemaProperty.getChildSchema(value);
if (needsQuotes)
insertionText = value + "\"";
if (existingFields != null && existingFields.contains(value))
continue;
proposals.add(createCompletionProposal(offset, value,
insertionText, childSchema.getDescription(),
stringToReplace));
}
}
}
/**
* Adds the pseudo parameters from the cloud formation schema to the list of
* proposals.
*
* @param offset
* The current cursor position.
* @param proposals
* List containing the proposals.
*/
private void addPseudoParameterCompletions(int offset,
List<ICompletionProposal> proposals) {
for (PseudoParameter parameter : schemaRules.getPseudoParameters()) {
proposals.add(createCompletionProposal(offset, parameter.getName(),
parameter.getName(), parameter.getDescription(),
EMPTY_STRING));
}
}
/**
* Adds the intrinsic functions from the cloud formation schema to the list
* of proposals.
*
* @param offset
* The current cursor position.
* @param proposals
* List containing the proposals.
*/
private void addIntrinsicFunctionCompletions(int offset,
List<ICompletionProposal> proposals) {
for (IntrinsicFunction function : schemaRules.getIntrinsicFuntions()) {
if (function.getName().equals("Ref")) {
// text, offset, 0, text.length(), null, text, contextInfo,
// description)
String refLiteral = "{ \"Ref\" : \"\" }";
String additionalProposalInfo = "Reference to a resource";
IContextInformation contextInfo = new ContextInformation("Ref",
additionalProposalInfo);
proposals.add(new CompletionProposal(refLiteral, offset, 0, 11,
null, "Ref", contextInfo, additionalProposalInfo));
} else {
proposals.add(createCompletionProposal(offset,
function.getName(), function.getName(),
function.getDescription(), EMPTY_STRING));
}
}
}
public char[] getCompletionProposalAutoActivationCharacters() {
return new char[] { '"' };
}
public IContextInformation[] computeContextInformation(ITextViewer viewer,
int offset) {
// TODO Auto-generated method stub
return null;
}
public char[] getContextInformationAutoActivationCharacters() {
// TODO Auto-generated method stub
return null;
}
public String getErrorMessage() {
// TODO Auto-generated method stub
return null;
}
public IContextInformationValidator getContextInformationValidator() {
// TODO Auto-generated method stub
return null;
}
}