/*
* Copyright (C) 2011 Google 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://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License 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.google.api.explorer.client.parameter.schema;
import com.google.api.explorer.client.base.ApiMethod;
import com.google.api.explorer.client.base.ApiMethod.HttpMethod;
import com.google.api.explorer.client.base.ApiService;
import com.google.api.explorer.client.base.Schema;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.HasText;
import com.google.gwt.user.client.ui.InlineLabel;
import com.google.gwt.user.client.ui.SimpleCheckBox;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
/**
* UI for constructing a request body based on the schema of the expected
* request.
*
* @author jasonhall@google.com (Jason Hall)
*/
public class SchemaForm extends Composite {
interface SchemaFormUiBinder extends UiBinder<Widget, SchemaForm> {
}
public @UiField HTMLPanel root;
private SchemaEditor editor;
private ApiMethod method;
public SchemaForm(UiBinder<Widget, SchemaForm> uiBinder) {
initWidget(uiBinder.createAndBindUi(this));
}
public SchemaForm() {
this((SchemaFormUiBinder) GWT.create(SchemaFormUiBinder.class));
}
/**
* Returns the JSON string value of the current state of the object shown in
* this form.
*/
public String getStringValue() {
if (editor == null) {
return "";
}
StringBuilder builder = new StringBuilder();
editor.prettyPrint(builder, 0);
return builder.toString();
}
/**
* Returns the JSON format of the current state of the object.
*/
public JSONObject getJSONObject() {
return editor.getJSONValue().isObject();
}
/**
* Set the value of the base editor to the JSON value recursively.
*
* @throws JSONException if the JSON can not be applied to the editor cleanly.
*/
public void setJSONValue(JSONValue value) {
// May not yet be initialized with a schema.
if (editor != null) {
editor.setJSONValue(value);
}
}
/** Sets the {@link Schema} to be displayed in this form. */
public void setSchema(ApiService service, ApiMethod method, Schema schema) {
this.method = method;
this.editor = getSchemaEditorForSchema(service, schema,
/* This is the root element and descendats should be nullable, if also patch. */ true);
root.clear();
root.add(editor.render(schema));
}
private boolean methodIsPatch() {
return method.getHttpMethod().equals(HttpMethod.PATCH);
}
/** Returns the SchemaEditor for the given Property value. */
SchemaEditor getSchemaEditorForSchema(
ApiService service, Schema schema, boolean descendantsNullable) {
Schema dereferenced = schema;
if (schema.getRef() != null) {
// Properties of this object are defined elsewhere.
dereferenced = service.getSchemas().get(schema.getRef());
}
SchemaEditor editor;
if (dereferenced.getType() != null) {
switch (dereferenced.getType()) {
case OBJECT:
editor = new ObjectSchemaEditor(this,
method.getId(),
service,
dereferenced.getProperties(),
dereferenced.getAdditionalProperties(),
methodIsPatch() && descendantsNullable);
break;
case ARRAY:
editor = new ArraySchemaEditor(service, this, dereferenced.getItems());
break;
case BOOLEAN:
editor = new BooleanSchemaEditor();
break;
case INTEGER:
case NUMBER:
editor = new NumberSchemaEditor();
break;
case ANY:
case STRING:
default:
editor = new StringSchemaEditor();
}
} else {
editor = new StringSchemaEditor();
}
return editor;
}
/** Base interface for all schema-based editors. */
interface SchemaEditor {
static final String INDENTATION = " ";
/** Returns a widget displaying the UI for the user to fill in. */
Widget render(Schema property);
/**
* Returns the JSON value of the single property defined displayed by this
* editor.
*/
JSONValue getJSONValue();
/**
* Recursively bind the JSON provided to the editors.
*
* @throws JSONException if the JSON can not be applied to the editor cleanly.
*/
void setJSONValue(JSONValue value);
/**
* Add results from our calculation on to the existing results.
*/
void prettyPrint(StringBuilder resultSoFar, int indentation);
/**
* Returns whether or not this schema editor is composed of other schema editors.
*/
boolean isComposite();
}
/** Editor for string values. */
static class StringSchemaEditor implements SchemaEditor {
private HasText hasText;
@Override
public Widget render(Schema property) {
HTMLPanel panel = new HTMLPanel("");
panel.getElement().getStyle().setDisplay(Display.INLINE);
panel.add(new InlineLabel("\""));
if (property.locked()) {
InlineLabel label = new InlineLabel();
panel.add(label);
hasText = label;
} else {
TextArea editor = new TextArea();
panel.add(editor);
hasText = editor;
}
panel.add(new InlineLabel("\""));
if (property.getDefault() != null) {
hasText.setText(property.getDefault());
}
return panel;
}
@Override
public JSONValue getJSONValue() {
return new JSONString(hasText.getText());
}
@Override
public void setJSONValue(JSONValue value) {
JSONString stringVal = value.isString();
if (stringVal != null) {
hasText.setText(stringVal.stringValue());
} else {
throw new JSONException("Not a valid JSON string: " + value.toString());
}
}
@Override
public void prettyPrint(StringBuilder resultSoFar, int indentation) {
resultSoFar.append(getJSONValue().toString());
}
@Override
public boolean isComposite() {
return false;
}
}
/** Editor for numerical values. */
static class NumberSchemaEditor implements SchemaEditor {
private TextBox textbox;
@Override
public Widget render(Schema property) {
textbox = new TextBox();
if (property.getDefault() != null) {
textbox.setValue(property.getDefault());
}
return textbox;
}
@Override
public JSONValue getJSONValue() {
// Try to parse the value as a number.
double val;
try {
val = Double.valueOf(textbox.getValue());
} catch (NumberFormatException nfe) {
// If the value is not a number, pass it as a string.
return new JSONString(textbox.getValue());
}
return new JSONNumber(val);
}
@Override
public void setJSONValue(JSONValue value) {
JSONNumber numberVal = value.isNumber();
JSONString stringVal = value.isString();
if (numberVal != null) {
textbox.setValue(String.valueOf(numberVal.doubleValue()));
} else if (stringVal != null){
textbox.setValue(stringVal.stringValue());
} else {
throw new JSONException("Not a valid JSON number: " + value.toString());
}
}
@Override
public void prettyPrint(StringBuilder resultSoFar, int indentation) {
resultSoFar.append(getJSONValue().toString());
}
@Override
public boolean isComposite() {
return false;
}
}
/** Editor for boolean values. */
static class BooleanSchemaEditor implements SchemaEditor {
private SimpleCheckBox checkbox;
@Override
public Widget render(Schema property) {
checkbox = new SimpleCheckBox();
// Set it to true if that is the default.
checkbox.setValue("true".equals(property.getDefault()));
// If this property is locked, disable the checkbox
if (property.locked()) {
checkbox.setEnabled(false);
}
return checkbox;
}
@Override
public JSONValue getJSONValue() {
return JSONBoolean.getInstance(checkbox.getValue());
}
@Override
public void setJSONValue(JSONValue value) {
JSONBoolean boolVal = value.isBoolean();
JSONString stringVal = value.isString();
if (boolVal != null) {
checkbox.setValue(boolVal.booleanValue());
} else if (stringVal != null) {
checkbox.setValue(Boolean.parseBoolean(stringVal.stringValue()));
} else {
throw new JSONException("Not a valid JSON boolean: " + value.toString());
}
}
@Override
public void prettyPrint(StringBuilder resultSoFar, int indentation) {
resultSoFar.append(getJSONValue().toString());
}
@Override
public boolean isComposite() {
return false;
}
}
}