// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.apphosting.utils.config;
import net.sourceforge.yamlbeans.YamlException;
import net.sourceforge.yamlbeans.YamlReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.List;
/**
* Class to parse index.yaml into a IndexesXml object.
*
*/
public class IndexYamlReader {
public static final String INDEX_DEFINITIONS_TAG =
"!!python/object:google.appengine.datastore.datastore_index.IndexDefinitions";
public static final String INDEX_TAG =
"!!python/object:google.appengine.datastore.datastore_index.Index";
public static final String PROPERTY_TAG =
"!!python/object:google.appengine.datastore.datastore_index.Property";
/**
* Wrapper around IndexesXml to make the JavaBeans properties match the YAML
* file syntax.
*/
public static class IndexYaml {
public String application;
/**
* JavaBean wrapper for Index entries in IndexesXml.
* This class must be kept in sync with apphosting/datastore/datastore_index.py
*/
public static class Index {
public String kind;
protected boolean ancestor;
public List<Property> properties;
public void setAncestor(String ancestor) {
this.ancestor = YamlUtils.parseBoolean(ancestor);
}
public String getAncestor() {
return "" + ancestor;
}
}
/**
* JavaBean wrapper for IndexesXml properties.
*/
public static class Property {
private String name = null;
private String direction = null;
private String mode = null;
public void setDirection(String direction) {
if ("desc".equals(direction) || "asc".equals(direction)) {
this.direction = direction;
} else {
throw new AppEngineConfigException(
"Invalid direction '" + direction + "': expected 'asc' or 'desc'.");
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDirection() {
return direction;
}
public void setMode(String mode) {
if (mode != null && mode.equals("null")) {
mode = null;
}
if (mode != null && !(mode.equals("geospatial") || mode.equals("segment"))) {
throw new AppEngineConfigException("Invalid mode: '" + mode);
}
this.mode = mode;
}
public String getMode() {
return mode;
}
}
private List<Index> indexes;
public List<Index> getIndexes() {
return indexes;
}
public void setIndexes(List<Index> indexes) {
this.indexes = indexes;
}
public IndexesXml toXml(IndexesXml xml) {
if (indexes == null) {
throw new AppEngineConfigException("Empty index configuration.");
}
if (xml == null) {
xml = new IndexesXml();
}
for (Index yamlIndex : indexes) {
if (yamlIndex.kind == null) {
throw new AppEngineConfigException("Index missing required element 'kind'");
}
IndexesXml.Index xmlIndex = xml.addNewIndex(yamlIndex.kind, yamlIndex.ancestor);
if (yamlIndex.properties != null) {
for (Property property : yamlIndex.properties) {
if (property.getName() == null) {
throw new AppEngineConfigException("Property is missing required element 'name'.");
}
xmlIndex.addNewProperty(
property.getName(), property.getDirection(), property.getMode());
}
}
}
return xml;
}
}
public static IndexesXml parse(Reader yaml, IndexesXml xml) {
List<IndexesXml> list;
try {
list = parseMultiple(yaml, xml);
} catch (YamlException ex) {
throw new AppEngineConfigException(ex.getMessage(), ex);
}
if (0 == list.size()) {
throw new AppEngineConfigException("Empty index configuration.");
}
if (list.size() > 1) {
throw new AppEngineConfigException(
"yaml unexepectedly contains more than one document: " + list.size());
}
return list.get(0);
}
/**
* Parses the Yaml from {@code yaml} into a Yaml document and deserializes
* the document into an instance of {@link IndexesXml}.
* @param yaml A {@link String} from which to read the Yaml. This String is allowed to
* be in the style generated by the admin server, including the Python-specific tags.
* This method will safely ignore those tags.
* @return An instance of {@link IndexesXml}.
*/
public static IndexesXml parse(String yaml) {
return parse(new StringReader(clean(yaml)), null);
}
/**
* Parses the Yaml from {@code yaml} into one or more documents, and
* deserializes the documents into one or more instances of {@link IndexesXml}
* which are returned in a {@link List}.
*
* @param yaml A {@link String} from which to read the Yyaml. This String is
* allowed to be in the style generated by the admin server, including
* the Python-specific tags. This method will safely ignore those tags.
* @return A {@link List} of {@link IndexesXml} instances representing one or
* more parsed Yaml documents.
*/
public static List<IndexesXml> parseMultiple(String yaml) {
try {
return parseMultiple(new StringReader(clean(yaml)), null);
} catch (YamlException ex) {
throw new AppEngineConfigException(ex.getMessage(), ex);
}
}
/**
* Cleans a Yaml String by removing Pyton-specific tags.
* These tags are written by the admin server when it generates
* Yaml representing indexes.
* @param yaml A {@link String} containing Yaml
* @return The cleaned {@link String}
*/
private static String clean(String yaml) {
return yaml
.replaceAll(INDEX_DEFINITIONS_TAG, "")
.replaceAll(INDEX_TAG, "")
.replaceAll(PROPERTY_TAG, "")
.trim();
}
/**
* Parses the Yaml from {@code yaml} into one or more documents, and
* deserializes the documents into one or more instances of {@link IndexesXml}
* which are returned in a {@link List}.
*
* @param yaml A {@link Reader} from which to read the yaml
* @param xml A possibly {@code null} {@link IndexesXml} instance. If this
* parameter is not {@code null} then each of the yaml documents will
* be deserialized into it. This parameter is intended to be
* used in cases where there is only one Yaml document expected and so
* there will only be one instance of {@link IndexesXml} in the
* returned {@link List}. If this parameter is not {@code null} and the
* returned list has length greater than 1, then the list will contain
* multiple copies of this parameter.
* @return A {@link List} of {@link IndexesXml} instances representing one or
* more parsed Yaml documents.
* @throws YamlException If the Yaml parser has trobule parsing.
*/
private static List<IndexesXml> parseMultiple(Reader yaml, IndexesXml xml) throws YamlException {
YamlReader reader = new YamlReader(yaml);
reader.getConfig().setPropertyElementType(IndexYaml.class, "indexes", IndexYaml.Index.class);
reader.getConfig().setPropertyElementType(
IndexYaml.Index.class, "properties", IndexYaml.Property.class);
List<IndexesXml> list = new LinkedList<IndexesXml>();
while (true) {
IndexYaml indexYaml = reader.read(IndexYaml.class);
if (null == indexYaml) {
break;
} else {
list.add(indexYaml.toXml(xml));
}
}
return list;
}
}