/*
* Copyright 2012 Vaadin Ltd.
*
* 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 org.vaadin.tori.component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.vaadin.tori.ToriApiLoader;
import org.vaadin.tori.ToriNavigator;
import org.vaadin.tori.data.entity.AbstractEntity;
import org.vaadin.tori.data.entity.Category;
import org.vaadin.tori.data.entity.DiscussionThread;
import org.vaadin.tori.data.entity.Post;
import org.vaadin.tori.exception.DataSourceException;
import org.vaadin.tori.mvp.AbstractView;
import org.vaadin.tori.service.DebugAuthorizationService;
import org.vaadin.tori.view.listing.ListingView;
import org.vaadin.tori.view.thread.ThreadView;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.server.Page;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.CssLayout;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Panel;
import com.vaadin.ui.PopupView;
import com.vaadin.ui.PopupView.PopupVisibilityEvent;
import com.vaadin.ui.PopupView.PopupVisibilityListener;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.Reindeer;
@SuppressWarnings("serial")
public class DebugControlPanel extends CustomComponent implements
PopupVisibilityListener {
private static class CheckBoxShouldBeDisabledException extends Exception {
}
@SuppressWarnings("unused")
private static class ContextData {
private Category category;
private DiscussionThread thread;
private List<Post> posts;
public void setCategory(final Category category) {
this.category = category;
}
public Category getCategory() {
return category;
}
public void setThread(final DiscussionThread thread) {
this.thread = thread;
}
public DiscussionThread getThread() {
return thread;
}
public void setPosts(final List<Post> posts) {
this.posts = posts;
}
public List<Post> getPosts() {
return posts;
}
}
private class CheckboxListener implements ValueChangeListener {
private final ContextData data;
private final Method setter;
public CheckboxListener(final ContextData data, final Method setter) {
this.data = data;
this.setter = setter;
}
@Override
public void valueChange(final ValueChangeEvent event) {
final boolean newValue = ((CheckBox) event.getProperty())
.getValue();
callSetter(newValue);
ToriNavigator navigator = ToriNavigator.getCurrent();
navigator.navigateTo(navigator.getState());
}
private void callSetter(final boolean newValue) {
try {
if (methodHasArguments(setter, 1)) {
setter.invoke(authorizationService, newValue);
}
else {
Class<?> paramClass = parseAbstractEntityclass(setter);
if (paramClass == Post.class) {
throw new IllegalStateException("Setters for "
+ Post.class.getName()
+ " should be handled by "
+ "another piece of code. MAJOR BUG!");
} else {
final Object setterParam = getCorrectTypeOfDataFrom(
paramClass, data);
if (setterParam instanceof AbstractEntity) {
setter.invoke(authorizationService,
((AbstractEntity) setterParam).getId(),
newValue);
}
}
}
} catch (final Exception e) {
Notification.show(e.getClass().getSimpleName());
e.printStackTrace();
}
}
}
private class PostCheckboxListener implements ValueChangeListener {
private final Post post;
private final Method setter;
public PostCheckboxListener(final Post post, final Method setter) {
this.post = post;
this.setter = setter;
}
@Override
public void valueChange(final ValueChangeEvent event) {
final boolean newValue = ((CheckBox) event.getProperty())
.getValue();
callSetter(newValue);
ToriNavigator navigator = ToriNavigator.getCurrent();
navigator.navigateTo(navigator.getState());
}
private void callSetter(final boolean newValue) {
try {
setter.invoke(authorizationService, post.getId(), newValue);
} catch (final Exception e) {
Notification.show(e.getClass().getSimpleName());
e.printStackTrace();
}
}
}
private final DebugAuthorizationService authorizationService;
private ContextData data;
protected com.vaadin.navigator.View currentView;
public DebugControlPanel(
final DebugAuthorizationService authorizationService) {
Page.getCurrent()
.getStyles()
.add(".v-popupview-popup { background: #fff; } .v-popupview-popup .v-widget { font-size: 12px; }");
addStyleName("debugcontrolpanel");
this.authorizationService = authorizationService;
ToriNavigator.getCurrent().addViewChangeListener(
new ViewChangeListener() {
@Override
public void afterViewChange(final ViewChangeEvent event) {
currentView = event.getNewView();
}
@Override
public boolean beforeViewChange(final ViewChangeEvent event) {
return true;
}
});
final PopupView popupButton = new PopupView("Debug Control Panel",
new Panel());
popupButton.setHideOnMouseOut(false);
popupButton.addStyleName("v-button");
popupButton.addPopupVisibilityListener(this);
setCompositionRoot(popupButton);
setSizeUndefined();
}
private ContextData getContextData() {
final ContextData data = new ContextData();
if (currentView instanceof AbstractView) {
String viewTitle = ((AbstractView) currentView).getTitle();
final Long urlParameterId = ((AbstractView) currentView)
.getUrlParameterId();
if (urlParameterId != null) {
if (currentView instanceof ListingView) {
try {
data.setCategory(ToriApiLoader.getCurrent()
.getDataSource().getCategory(urlParameterId));
} catch (DataSourceException e) {
e.printStackTrace();
}
} else if (currentView instanceof ThreadView) {
try {
DiscussionThread thread = ToriApiLoader.getCurrent()
.getDataSource().getThread(urlParameterId);
data.setThread(thread);
data.setCategory(thread.getCategory());
data.setPosts(thread.getPosts());
} catch (DataSourceException e) {
e.printStackTrace();
}
}
}
}
return data;
}
private Component createControlPanel(final ContextData data) {
this.data = data;
final VerticalLayout layout = new VerticalLayout();
layout.setStyleName(Reindeer.PANEL_LIGHT);
layout.setWidth("300px");
layout.setSpacing(true);
final Set<Method> setters = getSettersByReflection(authorizationService);
final List<Method> orderedSetters = new ArrayList<Method>(setters);
Collections.sort(orderedSetters, new Comparator<Method>() {
@Override
public int compare(final Method o1, final Method o2) {
return o1.getName().compareToIgnoreCase(o2.getName());
}
});
try {
for (final Method setter : orderedSetters) {
if (isForPosts(setter)) {
layout.addComponent(createPostControl(setter,
data.getPosts()));
} else {
layout.addComponent(createRegularControl(setter));
}
}
} catch (final Exception e) {
e.printStackTrace();
layout.addComponent(new Label(e.toString()));
}
return layout;
}
private Component createPostControl(final Method setter,
final List<Post> posts) throws Exception {
if (posts == null || posts.isEmpty()) {
final Label label = new Label(getNameForCheckBox(setter));
label.setEnabled(false);
return label;
}
Component content = new CustomComponent() {
{
final CssLayout root = new CssLayout();
root.addStyleName("postselect-content");
root.addStyleName(setter.getName());
setCompositionRoot(root);
root.setWidth("100%");
setWidth("400px");
root.addComponent(new Label(setter.getName()));
for (final Post post : posts) {
final Method getter = getGetterFrom(setter);
final boolean getterValue = (Boolean) getter.invoke(
authorizationService, post.getId());
final String authorName = post.getAuthor()
.getDisplayedName();
String postBody = post.getBodyRaw();
if (postBody.length() > 20) {
postBody = postBody.substring(0, 20);
}
final CheckBox checkbox = new CheckBox(authorName + " :: "
+ postBody);
checkbox.setValue(getterValue);
checkbox.addValueChangeListener(new PostCheckboxListener(
post, setter));
checkbox.setImmediate(true);
checkbox.setWidth("100%");
root.addComponent(checkbox);
}
}
};
final PopupView popup = new PopupView(getNameForCheckBox(setter),
content);
popup.setHideOnMouseOut(false);
popup.setHeight(30.0f, Unit.PIXELS);
return popup;
}
private Component createRegularControl(final Method setter)
throws SecurityException, NoSuchMethodException, Exception {
final CheckBox checkbox = new CheckBox(getNameForCheckBox(setter));
try {
final boolean getterValue = callGetter(getGetterFrom(setter));
checkbox.setValue(getterValue);
checkbox.addValueChangeListener(new CheckboxListener(data, setter));
checkbox.setImmediate(true);
} catch (final CheckBoxShouldBeDisabledException e) {
/*
* Because our context doesn't have data to set up this object, we
* disable the checkbox. E.g. DashboardView doesn't have context
* data on a particular Category or Thread
*/
checkbox.setEnabled(false);
}
return checkbox;
}
private String getNameForCheckBox(final Method setter) {
if (setter.getParameterTypes().length == 1) {
return setter.getName();
} else {
final List<String> typeNames = new ArrayList<String>();
for (final Class<?> type : setter.getParameterTypes()) {
typeNames.add(type.getSimpleName());
}
typeNames.remove(typeNames.size() - 1); // the last boolean
final String params = Arrays.toString(typeNames.toArray());
return setter.getName() + "(" + params + ")";
}
}
private static boolean isForPosts(final Method setter) {
return setter.getName().endsWith("Post");
}
private boolean callGetter(final Method getter)
throws CheckBoxShouldBeDisabledException, Exception {
if (methodHasArguments(getter, 0)) {
return (Boolean) getter.invoke(authorizationService);
} else if (methodHasArguments(getter, 1)) {
Class<?> paramClass = parseAbstractEntityclass(getter);
Object entityParameter = getCorrectTypeOfDataFrom(paramClass, data);
if (entityParameter instanceof AbstractEntity) {
return (Boolean) getter.invoke(authorizationService,
((AbstractEntity) entityParameter).getId());
} else {
throw new CheckBoxShouldBeDisabledException();
}
} else {
throw new IllegalArgumentException("Getter has too many parameters");
}
}
private Class<?> parseAbstractEntityclass(final Method getter) {
String name = getter.getName();
Class<?> result = null;
if (name.endsWith("Category")) {
result = Category.class;
} else if (name.endsWith("Thread")) {
result = DiscussionThread.class;
} else if (name.endsWith("Post")) {
result = List.class;
}
return result;
}
private Method getGetterFrom(final Method setter) throws SecurityException,
NoSuchMethodException {
if (setter.getParameterTypes().length == 1) {
// this is a simple, global access right.
final String getterSubString = setter.getName().substring(3);
final String getterName = getterSubString.substring(0, 1)
.toLowerCase() + getterSubString.substring(1);
for (final Method method : authorizationService.getClass()
.getMethods()) {
if (method.getName().equals(getterName)
&& method.getParameterTypes().length == 0) {
return method;
}
}
throw new NoSuchMethodException("No expected method " + getterName
+ "() was found in "
+ authorizationService.getClass().getName());
} else if (setter.getParameterTypes().length == 2) {
// this is a access right to a certain object.
final Class<?> type = setter.getParameterTypes()[0];
final String getterSubString = setter.getName().substring(3);
final String getterName = getterSubString.substring(0, 1)
.toLowerCase() + getterSubString.substring(1);
for (final Method method : authorizationService.getClass()
.getMethods()) {
if (method.getName().equals(getterName)
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0] == type) {
return method;
}
}
throw new NoSuchMethodException("No expected method " + getterName
+ "(" + type.getSimpleName() + ") was found in "
+ authorizationService.getClass().getName());
} else {
throw new RuntimeException(
"Setter has an unexpected amount of parameters. ARCHITECTURE BUG!");
}
}
private static Set<Method> getSettersByReflection(
final DebugAuthorizationService object) {
final Set<Method> setters = new HashSet<Method>();
for (final Method method : object.getClass().getMethods()) {
final boolean soundsLikeASetter = method.getName()
.startsWith("set");
if (soundsLikeASetter) {
if (isAnAcceptableSetter(method)) {
setters.add(method);
} else {
// if this happens, it's actually our own fault. Oops.
throw new IllegalStateException("method " + method
+ " sounds like a setter, but doesn't "
+ "conform to our format");
}
}
}
return setters;
}
private static boolean methodHasArguments(final Method method,
final int amount) {
return method.getParameterTypes().length == amount;
}
private static boolean isAnAcceptableSetter(final Method method) {
final Class<?>[] parameterTypes = method.getParameterTypes();
if (methodHasArguments(method, 1)) {
return parameterTypes[0] == boolean.class;
} else if (methodHasArguments(method, 2)) {
return (AbstractEntity.class.isAssignableFrom(parameterTypes[0])
|| parameterTypes[0].toString().equals("long") || parameterTypes[0] == Long.class)
&& parameterTypes[1] == boolean.class;
} else {
throw new IllegalArgumentException(
"Given method has 0 or more than 2 parameters");
}
}
private static <T extends Object> T getCorrectTypeOfDataFrom(
final Class<T> paramClass, final ContextData data) {
try {
for (final Method method : data.getClass().getMethods()) {
if (method.getReturnType() == paramClass
&& method.getParameterTypes().length == 0) {
@SuppressWarnings("unchecked")
final T value = (T) method.invoke(data);
return value;
}
}
} catch (final Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
throw new RuntimeException(ContextData.class.getName()
+ " does not have a no-arg method that "
+ "would return the data type " + paramClass);
}
@Override
public void popupVisibilityChange(final PopupVisibilityEvent event) {
final ContextData data = getContextData();
if (event.isPopupVisible()) {
Panel panel = (Panel) event.getPopupView().getContent()
.getPopupComponent();
panel.setContent(createControlPanel(data));
}
}
}