/*
* 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.Resources;
import com.google.api.explorer.client.base.ApiService;
import com.google.api.explorer.client.base.Schema;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.InlineLabel;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Tree-based UI for selecting fields to request, based on the response schema
* for a method.
*
* @author jasonhall@google.com (Jason Hall)
*/
public class FieldsEditor extends HTMLPanel implements HasValue<Boolean> {
private static final Joiner JOINER = Joiner.on(',').skipNulls();
private final ApiService service;
private final String key;
private final CheckBox root;
private final Map<String, HasValue<Boolean>> children = Maps.newHashMap();
public FieldsEditor(ApiService service, String key) {
super("");
this.service = service;
this.key = key;
root = new CheckBox(key.isEmpty() ? "Select all/none" : key);
root.setValue(false);
root.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event) {
for (HasValue<Boolean> checkBox : children.values()) {
checkBox.setValue(event.getValue(), true);
}
}
});
add(root);
}
/**
* Sets the properties this field will have, if it is an object.
*/
public void setProperties(Map<String, Schema> properties) {
List<String> keys = Lists.newArrayList(properties.keySet());
Collections.sort(keys);
HTMLPanel inner = new HTMLPanel("");
inner.getElement().getStyle().setPaddingLeft(20, Unit.PX);
for (String childKey : keys) {
final Schema property = properties.get(childKey);
final Map<String, Schema> childProperties = property.getProperties();
final Schema items = property.getItems();
if (childProperties == null && items == null) {
// This is a simple field
CheckBox checkBox = new CheckBox(childKey);
checkBox.setValue(root.getValue());
checkBox.setTitle(property.getDescription());
children.put(childKey, checkBox);
checkBox.getElement().appendChild(Document.get().createBRElement());
inner.add(checkBox);
} else {
final FieldsEditor editor = new FieldsEditor(service, childKey);
children.put(childKey, editor);
inner.add(editor);
if (childProperties != null) {
editor.setProperties(childProperties);
} else if (property.getRef() != null) {
editor.setRef(property.getRef());
} else if (items != null) {
if (items.getProperties() != null) {
editor.setProperties(items.getProperties());
} else if (items.getRef() != null) {
editor.setRef(items.getRef());
}
}
}
}
add(inner);
}
/**
* Denotes that this is an object whose definition is in another Schema, and
* that it should be filled in with the correct fields when expanded.
*/
void setRef(final String ref) {
final InlineLabel expando = new InlineLabel("+");
add(expando);
expando.addStyleName(Resources.INSTANCE.style().clickable());
expando.setTitle("Click to show more fields");
expando.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
Schema sch = service.getSchemas().get(ref);
setProperties(sch.getProperties());
remove(expando);
}
});
}
/** Returns the string for this field, including sub-fields if it has any. */
public String genFieldsString() {
// If there are no children, that means it's an expandable item that is not
// expanded. Return just the key in this case.
if (children.isEmpty() && root.getValue()) {
return key;
}
// If all fields are checked, rather than return
// "object(all,fields,in,the,object)" just return "object"
if (getValue()) {
return key;
}
// Recursively construct the fields string of all selected children.
String directFields =
JOINER.join(Iterables.transform(children.entrySet(),
new Function<Map.Entry<String, HasValue<Boolean>>, String>() {
@Override
public String apply(Entry<String, HasValue<Boolean>> input) {
if (input.getValue() instanceof FieldsEditor) {
return ((FieldsEditor) input.getValue()).genFieldsString();
} else {
return input.getValue().getValue() ? input.getKey() : null;
}
}
}));
if (directFields.isEmpty()) {
// No children were selected, so this object is not needed.
return null;
} else if (!directFields.contains(",")) {
// If only one field is checked, rather than return
// "object(onlyOneField)", return "object/onlyOneField"
if (key.isEmpty()) {
return directFields;
} else {
return key + '/' + directFields;
}
}
StringBuilder sb = new StringBuilder();
if (!key.isEmpty()) {
sb.append(key).append('(');
}
sb.append(directFields);
if (!key.isEmpty()) {
sb.append(')');
}
return sb.toString();
}
/**
* Returns this field's checked value, or if it has children, whether all its
* children are checked.
*/
@Override
public Boolean getValue() {
if (children.isEmpty()) {
return root.getValue();
}
return Iterables.all(children.entrySet(),
new Predicate<Map.Entry<String, HasValue<Boolean>>>() {
@Override
public boolean apply(Entry<String, HasValue<Boolean>> input) {
return input.getValue().getValue();
}
});
}
/**
* Sets this field's checked value, and all of its childrens' if it has any.
*/
@Override
public void setValue(Boolean value) {
for (HasValue<Boolean> hasValue : children.values()) {
hasValue.setValue(value);
}
this.root.setValue(value);
}
/** Adds a ValueChangeHandler to this field. */
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler<Boolean> handler) {
return root.addValueChangeHandler(handler);
}
/** Sets this checkboxes value. */
@Override
public void setValue(Boolean value, boolean fireEvents) {
root.setValue(value, fireEvents);
}
}