/*
* Copyright 2014 Corpuslinguistic working group Humboldt University Berlin.
*
* 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 annis.gui;
import static annis.gui.SidebarState.AUTO_HIDDEN;
import static annis.gui.SidebarState.AUTO_VISIBLE;
import static annis.gui.SidebarState.HIDDEN;
import static annis.gui.SidebarState.VISIBLE;
import annis.gui.components.ScreenshotMaker;
import annis.gui.components.SettingsStorage;
import annis.libgui.AnnisBaseUI;
import static annis.libgui.AnnisBaseUI.USER_LOGIN_ERROR;
import annis.libgui.AnnisUser;
import annis.libgui.Background;
import annis.libgui.Helper;
import annis.libgui.IDGenerator;
import annis.libgui.LoginDataLostException;
import annis.security.User;
import com.google.common.eventbus.Subscribe;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.vaadin.data.validator.EmailValidator;
import com.vaadin.server.FontAwesome;
import com.vaadin.server.Resource;
import com.vaadin.server.ThemeResource;
import com.vaadin.server.VaadinSession;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.JavaScript;
import com.vaadin.ui.JavaScriptFunction;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
import com.vaadin.ui.themes.BaseTheme;
import com.vaadin.ui.themes.ValoTheme;
import elemental.json.JsonArray;
import java.util.LinkedHashSet;
import org.json.JSONException;
import org.slf4j.LoggerFactory;
/**
* The ANNIS main toolbar. Handles login, showing the sidebar (if it exists),
* the screenshot making and some information windows.
*
* @author Thomas Krause <krauseto@hu-berlin.de>
*/
public class MainToolbar extends HorizontalLayout
implements LoginListener, ScreenshotMaker.ScreenshotCallback,
SettingsStorage.LoadedListener
{
private static final org.slf4j.Logger log = LoggerFactory.getLogger(
MainToolbar.class);
public enum NavigationTarget
{
SEARCH(SearchView.NAME, "Search interface", FontAwesome.SEARCH),
ADMIN(AdminView.NAME, "Administration", FontAwesome.WRENCH);
private final String caption;
private final String state;
private final Resource icon;
private NavigationTarget(String state, String caption, Resource icon)
{
this.caption = caption;
this.state = state;
this.icon = icon;
}
}
private Button btSidebar;
private final Button btNavigate;
private NavigationTarget navigationTarget;
private final Button btLogin;
private final Button btLogout;
private final Button btBugReport;
private final Button btAboutAnnis;
private final Button btOpenSource;
private final Label lblUserName;
private final String bugEMailAddress;
private final LoginWindow windowLogin = new LoginWindow();
private SidebarState sidebarState = SidebarState.VISIBLE;
private final LinkedHashSet<LoginListener> loginListeners = new LinkedHashSet<>();
private Throwable lastBugReportCause;
private Sidebar sidebar;
private ScreenshotMaker screenshotExtension;
public static final String BUG_MAIL_KEY = "bug-e-mail";
public static final String LOGIN_URL_KEY = "login-url";
public static final String LOGIN_MAXIMIZED_KEY = "login-window-maximized";
private QueryController queryController;
public MainToolbar()
{
String bugmail = (String) VaadinSession.getCurrent().getAttribute(
BUG_MAIL_KEY);
if (bugmail != null && !bugmail.isEmpty()
&& !bugmail.startsWith("${")
&& new EmailValidator("").isValid(bugmail))
{
this.bugEMailAddress = bugmail;
}
else
{
this.bugEMailAddress = null;
}
UI ui = UI.getCurrent();
if (ui instanceof CommonUI)
{
((CommonUI) ui).getSettings().addedLoadedListener(MainToolbar.this);
}
setWidth("100%");
setHeight("-1px");
addStyleName("toolbar");
addStyleName("border-layout");
btAboutAnnis = new Button("About ANNIS");
btAboutAnnis.addStyleName(ValoTheme.BUTTON_SMALL);
btAboutAnnis.setIcon(new ThemeResource("images/annis_16.png"));
btAboutAnnis.addClickListener(new AboutClickListener());
btSidebar = new Button();
btSidebar.setDisableOnClick(true);
btSidebar.addStyleName(ValoTheme.BUTTON_SMALL);
btSidebar.setDescription("Show and hide search sidebar");
btSidebar.setIconAlternateText(btSidebar.getDescription());
btBugReport = new Button("Report Problem");
btBugReport.addStyleName(ValoTheme.BUTTON_SMALL);
btBugReport.setDisableOnClick(true);
btBugReport.setIcon(FontAwesome.ENVELOPE_O);
btBugReport.addClickListener(new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
reportBug();
}
});
btBugReport.setVisible(this.bugEMailAddress != null);
btNavigate = new Button();
btNavigate.setVisible(false);
btNavigate.setDisableOnClick(true);
btNavigate.addClickListener(new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
btNavigate.setEnabled(true);
if (navigationTarget != null)
{
UI.getCurrent().getNavigator().navigateTo(navigationTarget.state);
}
}
});
lblUserName = new Label("not logged in");
lblUserName.setWidth("-1px");
lblUserName.setHeight("-1px");
lblUserName.addStyleName("right-aligned-text");
btLogin = new Button("Login", new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
showLoginWindow(false);
}
});
btLogout = new Button("Logout", new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
// logout
Helper.setUser(null);
for (LoginListener l : loginListeners)
{
l.onLogout();
}
Notification.show("Logged out", Notification.Type.TRAY_NOTIFICATION);
updateUserInformation();
}
});
btLogin.setSizeUndefined();
btLogin.setStyleName(ValoTheme.BUTTON_SMALL);
btLogin.setIcon(FontAwesome.USER);
btLogout.setSizeUndefined();
btLogout.setStyleName(ValoTheme.BUTTON_SMALL);
btLogout.setIcon(FontAwesome.USER);
btOpenSource = new Button("Help us to make ANNIS better!");
btOpenSource.setStyleName(BaseTheme.BUTTON_LINK);
btOpenSource.addClickListener(new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
Window w = new HelpUsWindow();
w.setCaption("Help us to make ANNIS better!");
w.setModal(true);
w.setResizable(true);
w.setWidth("600px");
w.setHeight("500px");
UI.getCurrent().addWindow(w);
w.center();
}
});
addComponent(btSidebar);
setComponentAlignment(btSidebar, Alignment.MIDDLE_LEFT);
addComponent(btAboutAnnis);
addComponent(btBugReport);
addComponent(btNavigate);
addComponent(btOpenSource);
setSpacing(true);
setComponentAlignment(btAboutAnnis, Alignment.MIDDLE_LEFT);
setComponentAlignment(btBugReport, Alignment.MIDDLE_LEFT);
setComponentAlignment(btNavigate, Alignment.MIDDLE_LEFT);
setComponentAlignment(btOpenSource, Alignment.MIDDLE_CENTER);
setExpandRatio(btOpenSource, 1.0f);
addLoginButton();
btSidebar.addClickListener(new Button.ClickListener()
{
@Override
public void buttonClick(Button.ClickEvent event)
{
btSidebar.setEnabled(true);
// decide new state
switch (sidebarState)
{
case VISIBLE:
if (event.isCtrlKey())
{
sidebarState = SidebarState.AUTO_VISIBLE;
}
else
{
sidebarState = SidebarState.HIDDEN;
}
break;
case HIDDEN:
if (event.isCtrlKey())
{
sidebarState = SidebarState.AUTO_HIDDEN;
}
else
{
sidebarState = SidebarState.VISIBLE;
}
break;
case AUTO_VISIBLE:
if (event.isCtrlKey())
{
sidebarState = SidebarState.VISIBLE;
}
else
{
sidebarState = SidebarState.AUTO_HIDDEN;
}
break;
case AUTO_HIDDEN:
if (event.isCtrlKey())
{
sidebarState = SidebarState.HIDDEN;
}
else
{
sidebarState = SidebarState.AUTO_VISIBLE;
}
break;
}
updateSidebarState();
}
});
screenshotExtension = new ScreenshotMaker(this);
JavaScript.getCurrent().addFunction("annis.gui.logincallback",
new LoginCloseCallback());
updateSidebarState();
MainToolbar.this.updateUserInformation();
}
@Override
public void attach()
{
super.attach();
UI ui = UI.getCurrent();
if (ui instanceof AnnisBaseUI)
{
((AnnisBaseUI) ui).getLoginDataLostBus().register(this);
}
IDGenerator.assignIDForFields(MainToolbar.this , btAboutAnnis, btOpenSource);
}
@Override
public void detach()
{
UI ui = UI.getCurrent();
if (ui instanceof AnnisBaseUI)
{
((AnnisBaseUI) ui).getLoginDataLostBus().unregister(this);
}
super.detach();
}
public void setNavigationTarget(NavigationTarget target)
{
if(target == this.navigationTarget)
{
// nothing changed, return
}
this.navigationTarget = target;
btNavigate.setVisible(false);
if (target == NavigationTarget.ADMIN)
{
// check in background if display is necessary
AnnisUser user = Helper.getUser();
if (user != null && user.getUserName() != null)
{
Background.run(new CheckIfUserIsAdministratorJob(user.getUserName(), UI.
getCurrent()));
}
}
else if (target != null)
{
btNavigate.setVisible(true);
btNavigate.setCaption(target.caption);
btNavigate.setIcon(target.icon);
}
}
private void updateSidebarState()
{
if (sidebar != null && btSidebar != null)
{
btSidebar.setIcon(sidebarState.getIcon());
sidebar.updateSidebarState(sidebarState);
}
}
public void notifiyQueryStarted()
{
if (sidebarState == SidebarState.AUTO_VISIBLE)
{
sidebarState = SidebarState.AUTO_HIDDEN;
}
updateSidebarState();
}
public void addLoginListener(LoginListener listener)
{
this.loginListeners.add(listener);
}
public void removeLoginListener(LoginListener listener)
{
this.loginListeners.remove(listener);
}
/**
* Adds the login button + login text to the toolbar. This is only happened,
* when the gui is not started via the kickstarter.
*
* <p>
* The Kickstarter overrides the "kickstarterEnvironment" context parameter
* and set it to "true", so the gui can detect, that is not necessary to offer
* a login button.</p>
*
* component.
*/
private void addLoginButton()
{
VaadinSession session = VaadinSession.getCurrent();
if (session != null)
{
boolean kickstarter = Helper.isKickstarter(session);
if (!kickstarter)
{
addComponent(lblUserName);
setComponentAlignment(lblUserName, Alignment.MIDDLE_RIGHT);
addComponent(btLogin);
setComponentAlignment(btLogin, Alignment.MIDDLE_RIGHT);
}
}
}
@Override
public void onSettingsLoaded(SettingsStorage settings)
{
String sidebarStateSetting = settings.get("annis-sidebar-state");
if (sidebarStateSetting != null)
{
try
{
sidebarState = SidebarState.valueOf(sidebarStateSetting);
// don't be invisible
if (sidebarState == SidebarState.AUTO_HIDDEN)
{
sidebarState = SidebarState.AUTO_VISIBLE;
}
else if (sidebarState == SidebarState.HIDDEN)
{
sidebarState = SidebarState.VISIBLE;
}
}
catch (IllegalArgumentException ex)
{
log.debug("Invalid cookie for sidebar state", ex);
}
}
updateSidebarState();
}
private void updateUserInformation()
{
if (lblUserName == null)
{
return;
}
if (navigationTarget == NavigationTarget.ADMIN)
{
// don't show administration link per default
btNavigate.setVisible(false);
}
AnnisUser user = Helper.getUser();
// always close the window
if (windowLogin != null)
{
windowLogin.close(user != null);
}
if (user == null)
{
Object loginErrorOject = VaadinSession.getCurrent().getSession().
getAttribute(USER_LOGIN_ERROR);
if (loginErrorOject != null && loginErrorOject instanceof String)
{
Notification.show((String) loginErrorOject,
Notification.Type.WARNING_MESSAGE);
}
VaadinSession.getCurrent().getSession().removeAttribute(
AnnisBaseUI.USER_LOGIN_ERROR);
lblUserName.setValue("not logged in");
if (getComponentIndex(btLogout) > -1)
{
replaceComponent(btLogout, btLogin);
setComponentAlignment(btLogin, Alignment.MIDDLE_RIGHT);
}
}
else
{
// logged in
if (user.getUserName() != null)
{
Notification.show("Logged in as \"" + user.getUserName() + "\"",
Notification.Type.TRAY_NOTIFICATION);
lblUserName.setValue("logged in as \"" + user.
getUserName() + "\"");
}
if (getComponentIndex(btLogin) > -1)
{
replaceComponent(btLogin, btLogout);
setComponentAlignment(btLogout, Alignment.MIDDLE_RIGHT);
}
// do not show the logout button if the user cannot logout using ANNIS
btLogout.setVisible(!user.isRemote());
if (navigationTarget == NavigationTarget.ADMIN)
{
// check in background if display is necessary
if (user.getUserName() != null)
{
Background.run(new CheckIfUserIsAdministratorJob(user.getUserName(),
UI.getCurrent()));
}
}
}
}
@Override
public void onLogin()
{
updateUserInformation();
}
@Override
public void onLogout()
{
if (windowLogin != null)
{
// make sure to close the login window without triggering a search execution
windowLogin.close(false);
}
updateUserInformation();
}
public boolean canReportBugs()
{
return this.bugEMailAddress != null;
}
public void reportBug()
{
reportBug(null);
}
public void reportBug(Throwable cause)
{
lastBugReportCause = cause;
if (screenshotExtension.isAttached())
{
screenshotExtension.makeScreenshot();
btBugReport.setCaption("problem report is initialized...");
}
else
{
Notification.show(
"This user interface does not allow screenshots. Can't report bug.",
Notification.Type.ERROR_MESSAGE);
}
}
@Override
public void screenshotReceived(byte[] imageData, String mimeType)
{
btBugReport.setEnabled(true);
btBugReport.setCaption("Report Problem");
if (bugEMailAddress != null)
{
ReportBugWindow reportBugWindow
= new ReportBugWindow(bugEMailAddress, imageData, mimeType,
lastBugReportCause);
reportBugWindow.setModal(true);
reportBugWindow.setResizable(true);
UI.getCurrent().addWindow(reportBugWindow);
reportBugWindow.center();
lastBugReportCause = null;
}
}
public ScreenshotMaker getScreenshotExtension()
{
return screenshotExtension;
}
private static class AboutClickListener implements Button.ClickListener
{
public AboutClickListener()
{
}
@Override
public void buttonClick(Button.ClickEvent event)
{
Window w = new AboutWindow();
w.setCaption("About ANNIS");
w.setModal(true);
w.setResizable(true);
w.setWidth("500px");
w.setHeight("500px");
UI.getCurrent().addWindow(w);
}
}
public boolean isLoggedIn()
{
return Helper.getUser() != null;
}
private class LoginCloseCallback implements JavaScriptFunction
{
@Override
public void call(JsonArray arguments) throws JSONException
{
if (isLoggedIn())
{
for (LoginListener l : loginListeners)
{
try
{
l.onLogin();
}
catch (Exception ex)
{
log.error("exception thrown while notifying login listeners", ex);
}
}
}
updateUserInformation();
}
}
@Subscribe
public void handleLoginDataLostException(LoginDataLostException ex)
{
Notification.show("Login data was lost, please login again.",
"Due to a server misconfiguration the login-data was lost. Please contact the adminstrator of this ANNIS instance.",
Notification.Type.WARNING_MESSAGE);
for (LoginListener l : loginListeners)
{
try
{
l.onLogout();
}
catch (Exception loginEx)
{
log.error("exception thrown while notifying login listeners", loginEx);
}
}
updateUserInformation();
}
public Sidebar getSidebar()
{
return sidebar;
}
public void setSidebar(Sidebar sidebar)
{
this.sidebar = sidebar;
btSidebar.setVisible(sidebar != null);
updateSidebarState();
}
private class CheckIfUserIsAdministratorJob implements Runnable
{
private final String userName;
private final UI ui;
public CheckIfUserIsAdministratorJob(String userName, UI ui)
{
this.userName = userName;
this.ui = ui;
}
@Override
public void run()
{
User user = null;
try
{
user = Helper.getAnnisWebResource().path("admin/users").path(
userName)
.get(User.class);
}
catch(UniformInterfaceException ex)
{
// ignore
}
finally
{
boolean hasAdmistrationRights = false;
if (user != null)
{
for (String perm : user.getPermissions())
{
if (perm.startsWith("*:") || perm.startsWith("admin:"))
{
// the user has at least some administration rights
hasAdmistrationRights = true;
}
}
}
if (hasAdmistrationRights)
{
ui.access(new Runnable()
{
@Override
public void run()
{
// make the administration button visible
btNavigate.setCaption(NavigationTarget.ADMIN.caption);
btNavigate.setIcon(NavigationTarget.ADMIN.icon);
btNavigate.setVisible(true);
}
});
}
}
}
}
public void showLoginWindow(boolean executeQueryAfterLogin)
{
windowLogin.setExecuteSearchAfterClose(executeQueryAfterLogin);
if(windowLogin.isAttached())
{
windowLogin.close();
}
UI.getCurrent().addWindow(windowLogin);
}
public QueryController getQueryController()
{
return queryController;
}
public void setQueryController(QueryController queryController)
{
this.queryController = queryController;
windowLogin.setQueryController(queryController);
}
}