Java tutorial
/* * 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; } }