/*
* 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.ApiService;
import com.google.api.explorer.client.base.Schema;
import com.google.api.explorer.client.parameter.schema.SchemaForm.SchemaEditor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.json.client.JSONException;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
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.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Widget;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* {@link SchemaEditor} for object values. The keys/values of the object will
* have their own editors which will provide the string value of this editor.
*
* @author jasonhall@google.com (Jason Hall)
*/
class ObjectSchemaEditor extends Composite implements SchemaEditor {
private static ObjectSchemaEditorUiBinder uiBinder = GWT.create(ObjectSchemaEditorUiBinder.class);
interface ObjectSchemaEditorUiBinder extends UiBinder<Widget, ObjectSchemaEditor> {
}
private static final String ADD_PROPERTY = "-- add a property --";
private static final String EMPTY_INITIAL_KEY = "";
private static final boolean REQUIRED_PROPERTY = true;
private static final boolean OPTIONAL_PROPERTY = false;
private final SchemaForm schemaForm;
private final Map<String, Schema> properties;
private final ApiService service;
private final List<String> availableKeys = Lists.newArrayList();
private final String methodName;
private final Schema additionalPropertiesType;
private final boolean nullableValues;
@VisibleForTesting
final Map<String, SchemaEditor> editors = Maps.newHashMap();
@VisibleForTesting
final Set<AdditionalPropertyElement> additionalPropertyEditors = Sets.newHashSet();
@UiField
ListBox listBox;
@UiField
HTMLPanel panel;
@UiField
Label newItem;
@UiHandler("newItem")
void addNewAdditionalEditor(ClickEvent event) {
addAdditionalPropertyEditor(EMPTY_INITIAL_KEY);
}
ObjectSchemaEditor(SchemaForm schemaForm,
String methodName,
ApiService service,
Map<String, Schema> properties,
@Nullable Schema additionalPropertiesType,
boolean nullableValues) {
initWidget(uiBinder.createAndBindUi(this));
this.schemaForm = schemaForm;
this.properties = Objects.firstNonNull(properties, Collections.<String, Schema>emptyMap());
this.service = service;
this.methodName = methodName;
this.additionalPropertiesType = additionalPropertiesType;
this.nullableValues = nullableValues;
newItem.setVisible(additionalPropertiesType != null);
listBox.setVisible(!this.properties.isEmpty());
}
@Override
public Widget render(Schema ignored) {
clear();
return this;
}
public void clear() {
panel.clear();
editors.clear();
additionalPropertyEditors.clear();
availableKeys.clear();
availableKeys.addAll(properties.keySet());
Collections.sort(availableKeys);
// Iterate over properties in this object inspecting its annotations.
// Annotations tell us whether the parameter is required, or immutable.
for (Map.Entry<String, Schema> entry : properties.entrySet()) {
boolean required = entry.getValue().requiredForMethod(methodName)
|| entry.getValue().isRequired();
boolean immutable = !entry.getValue().mutableForMethod(methodName);
if (required) {
// Add all required fields for the selected method to the object form.
onSelect(entry.getKey(), REQUIRED_PROPERTY);
}
// TODO(jasonhall): Check if the property is immutable and remove it from
// availableKeys, when Discovery contains this information.
}
buildListBox();
listBox.addChangeHandler(new ChangeHandler() {
@Override
public void onChange(ChangeEvent event) {
onSelect(null, OPTIONAL_PROPERTY);
}
});
listBox.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent event) {
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
onSelect(null, OPTIONAL_PROPERTY);
}
}
});
}
@Override
public JSONValue getJSONValue() {
JSONObject obj = new JSONObject();
for (Map.Entry<String, SchemaEditor> entry : allEditors()) {
obj.put(entry.getKey(), entry.getValue().getJSONValue());
}
return obj;
}
@Override
public void setJSONValue(JSONValue value) {
JSONObject obj = value.isObject();
if (obj == null) {
// If this object came as a json blob, we might have to deserialize it
JSONString str = value.isString();
JSONValue parsed = null;
try {
parsed = JSONParser.parseStrict(str.stringValue());
} catch (Exception e) {
// There was an error parsing, just leave parsed as null
}
JSONObject parsedObject = parsed != null ? parsed.isObject() : null;
if (parsedObject != null) {
obj = parsed.isObject();
}
}
if (obj != null) {
// Clear the editor before we start adding the keys back in
clear();
// For each key that we are going to map we have to instantiate an
// appropriate editor type. The {@link #onSelect(String)} function
// instantiates the proper editor type for the key and binds the new
// editor to our editor.
for (String key : obj.keySet()) {
if (properties.containsKey(key)) {
SchemaEditor editor = onSelect(key, OPTIONAL_PROPERTY);
editor.setJSONValue(obj.get(key));
} else if (additionalPropertiesType != null){
SchemaEditor editor = addAdditionalPropertyEditor(key);
editor.setJSONValue(obj.get(key));
} else {
throw new JSONException("JSON object contains unknown key: " + key);
}
}
} else {
throw new JSONException("Invalid JSON object: " + value.toString());
}
}
@Override
public void prettyPrint(StringBuilder resultSoFar, int indentation) {
if (resultSoFar.length() > 0) {
resultSoFar.append("\n");
}
resultSoFar.append(Strings.repeat(INDENTATION, indentation)).append("{");
boolean first = true;
// Add the properties with fixed keys.
for (Map.Entry<String, SchemaEditor> entry : allEditors()) {
if (!first) {
resultSoFar.append(",");
}
first = false;
resultSoFar
.append("\n")
.append(Strings.repeat(INDENTATION, indentation + 1))
.append("\"")
.append(entry.getKey())
.append("\": ");
entry.getValue().prettyPrint(resultSoFar, indentation + 1);
}
resultSoFar.append("\n").append(Strings.repeat(INDENTATION, indentation)).append("}");
}
@VisibleForTesting
SchemaEditor onSelect(String key, boolean isRequired) {
// Selecting the first item in the list (a placeholder) has no effect.
if (listBox.getSelectedIndex() == 0 && key == null) {
return null;
}
// There may already be an editor for this key, if so just return it.
if (editors.containsKey(key)) {
return editors.get(key);
}
String selectedKey = key == null ? listBox.getValue(listBox.getSelectedIndex()) : key;
Schema selectedProperty = properties.get(selectedKey);
SchemaEditor editor = schemaForm.getSchemaEditorForSchema(service, selectedProperty,
/* Descendants inherit nullability. */ nullableValues);
boolean isRemovable = !isRequired && !selectedProperty.locked();
final ObjectElement row =
new ObjectElement(selectedKey, editor, selectedProperty, isRemovable, nullableValues);
panel.add(row);
editors.put(selectedKey, row);
// Remove the selected key from the listbox.
availableKeys.remove(selectedKey);
for (int i = 1; i < listBox.getItemCount(); i++) {
if (listBox.getItemText(i).equals(selectedKey)) {
listBox.removeItem(i);
break;
}
}
// If there aren't any keys left, hide the listbox.
if (availableKeys.isEmpty()) {
listBox.setVisible(false);
}
// When a row is removed, re-add its key to the list of available keys.
row.registerRemoveClickedHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
panel.remove(row);
editors.remove(row.key);
availableKeys.add(row.key);
Collections.sort(availableKeys);
buildListBox();
}
});
return row;
}
private SchemaEditor addAdditionalPropertyEditor(String initialKeyValue) {
SchemaEditor editor = schemaForm.getSchemaEditorForSchema(service, additionalPropertiesType,
/* Descendants inherit nullability. */ nullableValues);
final AdditionalPropertyElement row =
new AdditionalPropertyElement(editor, additionalPropertiesType);
// If the editor was created with an initial key, set it now.
row.setKeyValue(Preconditions.checkNotNull(initialKeyValue));
// Add our new components to the parent object and editor list.
panel.add(row);
additionalPropertyEditors.add(row);
row.remove.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent arg0) {
panel.remove(row);
additionalPropertyEditors.remove(row);
}
});
return editor;
}
/**
* Resets the listbox to contain all keys in availableKeys, and the
* placeholder, and sets the listbox visible.
*/
private void buildListBox() {
listBox.clear();
listBox.addItem(ADD_PROPERTY);
// In some cases, all keys will be required
if (!availableKeys.isEmpty()) {
for (String key : availableKeys) {
listBox.addItem(key);
}
listBox.setVisible(true);
}
}
private Iterable<Map.Entry<String, SchemaEditor>> allEditors() {
// Transform that map by extracting the editors.
Iterable<Map.Entry<String, SchemaEditor>> keysToEditors =
Iterables.transform(additionalPropertyEditors, AdditionalPropertyElement.normalizeEditor);
// Concatenate with the named editors.
return Iterables.concat(keysToEditors, editors.entrySet());
}
@Override
public boolean isComposite() {
return true;
}
}