/*
* Copyright 2011 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.libgui;
import annis.VersionInfo;
import annis.libgui.media.MediaController;
import annis.libgui.visualizers.VisualizerPlugin;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import com.google.common.base.Charsets;
import com.google.common.eventbus.EventBus;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import com.vaadin.annotations.Theme;
import com.vaadin.sass.internal.ScssStylesheet;
import com.vaadin.server.ClassResource;
import com.vaadin.server.Page;
import com.vaadin.server.RequestHandler;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinResponse;
import com.vaadin.server.VaadinService;
import com.vaadin.server.VaadinSession;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.ComponentContainer;
import com.vaadin.ui.HasComponents;
import com.vaadin.ui.UI;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.xeoh.plugins.base.Plugin;
import net.xeoh.plugins.base.PluginManager;
import net.xeoh.plugins.base.impl.PluginManagerFactory;
import net.xeoh.plugins.base.util.PluginManagerUtil;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.map.AnnotationIntrospector;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.xc.JaxbAnnotationIntrospector;
import org.slf4j.LoggerFactory;
import org.slf4j.bridge.SLF4JBridgeHandler;
/**
* Basic UI functionality.
*
* This class allows to out source some common tasks like initialization of
* the logging framework or the plugin loading to this base class.
*/
@Theme("annis")
public class AnnisBaseUI extends UI implements PluginSystem, Serializable
{
static
{
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
}
private static final org.slf4j.Logger log = LoggerFactory.getLogger(
AnnisBaseUI.class);
public final static String USER_KEY = "annis.gui.AnnisBaseUI:USER_KEY";
public final static String USER_LOGIN_ERROR = "annis.gui.AnnisBaseUI:USER_LOGIN_ERROR";
public final static String CONTEXT_PATH = "annis.gui.AnnisBaseUI:CONTEXT_PATH";
public final static String WEBSERVICEURL_KEY = "annis.gui.AnnisBaseUI:WEBSERVICEURL_KEY";
public final static String CITATION_KEY = "annis.gui.AnnisBaseUI:CITATION_KEY";
private transient PluginManager pluginManager;
private static final Map<String, VisualizerPlugin> visualizerRegistry =
Collections.synchronizedMap(new HashMap<String, VisualizerPlugin>());
private static final Map<String, Date> resourceAddedDate =
Collections.synchronizedMap(new HashMap<String, Date>());
private transient MediaController mediaController;
private transient ObjectMapper jsonMapper;
private TreeSet<String> alreadyAddedCSS = new TreeSet<String>();
private transient EventBus loginDataLostBus;
@Override
protected void init(VaadinRequest request)
{
initLogging();
// store the webservice URL property explicitly in the session in order to
// access it from the "external" servlets
getSession().getSession().setAttribute(WEBSERVICEURL_KEY,
getSession().getAttribute(Helper.KEY_WEB_SERVICE_URL));
getSession().setAttribute(CONTEXT_PATH, request.getContextPath());
alreadyAddedCSS.clear();
initPlugins();
checkIfRemoteLoggedIn(request);
getSession().addRequestHandler(new RemoteUserRequestHandler());
}
@Override
public void attach()
{
super.attach();
alreadyAddedCSS.clear();
}
@Override
public void close()
{
if (pluginManager != null)
{
pluginManager.shutdown();
}
super.close();
}
/**
* Given an configuration file name (might include directory) this function
* returns all locations for this file in the "ANNIS configuration system".
*
* The files in the result list do not necessarily exist.
*
* These locations are the
* - base installation: WEB-INF/conf/ folder of the deployment.
* - global configuration: $ANNIS_CFG environment variable value or /etc/annis/ if not set
* - user configuration: ~/.annis/
* @param configFile The file path of the configuration file relative to the base config folder.
* @return list of files or directories in the order in which they should be processed (most important is last)
*/
public static List<File> getAllConfigLocations(String configFile)
{
LinkedList<File> locations = new LinkedList<File>();
// first load everything from the base application
locations.add(new File(VaadinService.getCurrent().getBaseDirectory(),
"/WEB-INF/conf/" + configFile));
// next everything from the global config
// When ANNIS_CFG environment variable is set use this value or default to
// "/etc/annis/
String globalConfigDir = System.getenv("ANNIS_CFG");
if (globalConfigDir == null)
{
globalConfigDir = "/etc/annis";
}
locations.add(new File(globalConfigDir + "/" + configFile));
// the final and most specific user configuration is in the users home directory
locations.add(new File(
System.getProperty("user.home") + "/.annis/" + configFile));
return locations;
}
protected Map<String, InstanceConfig> loadInstanceConfig()
{
TreeMap<String, InstanceConfig> result = new TreeMap<>();
// get a list of all directories that contain instance informations
List<File> locations = getAllConfigLocations("instances");
for(File root : locations)
{
if(root.isDirectory())
{
// get all sub-files ending on ".json"
File[] instanceFiles =
root.listFiles((FilenameFilter) new SuffixFileFilter(".json"));
if(instanceFiles != null)
{
for(File i : instanceFiles)
{
if(i.isFile() && i.canRead())
{
try
{
InstanceConfig config = getJsonMapper().readValue(i, InstanceConfig.class);
String name = StringUtils.removeEnd(i.getName(), ".json");
config.setInstanceName(name);
result.put(name, config);
}
catch (IOException ex)
{
log.warn("could not parse instance config: " + ex.getMessage());
}
}
}
}
}
}
// always provide a default instance
if(!result.containsKey("default"))
{
InstanceConfig cfgDefault = new InstanceConfig();
cfgDefault.setInstanceDisplayName("ANNIS");
result.put("default", cfgDefault);
}
return result;
}
protected final void initLogging()
{
try
{
List<File> logbackFiles = getAllConfigLocations("gui-logback.xml");
InputStream inStream = null;
if(!logbackFiles.isEmpty())
{
try
{
inStream = new FileInputStream(logbackFiles.get(logbackFiles.size()-1));
}
catch(FileNotFoundException ex)
{
// well no logging no error...
}
}
if(inStream == null)
{
ClassResource res = new ClassResource(AnnisBaseUI.class, "logback.xml");
inStream = res.getStream().getStream();
}
if (inStream != null)
{
LoggerContext context = (LoggerContext) LoggerFactory.
getILoggerFactory();
JoranConfigurator jc = new JoranConfigurator();
jc.setContext(context);
context.reset();
context.putProperty("webappHome",
VaadinService.getCurrent().getBaseDirectory().getAbsolutePath());
// load config file
jc.doConfigure(inStream);
}
}
catch (JoranException ex)
{
log.error("init logging failed", ex);
}
}
/**
* Override this method to append additional plugins to the internal {@link PluginManager}.
*
* The default implementation is empty
* (thus you don't need to call {@code super.addCustomUIPlugins(...)}).
* @param pluginManager
*/
protected void addCustomUIPlugins(PluginManager pluginManager)
{
// default: do nothing
}
private void initPlugins()
{
log.info("Adding plugins");
pluginManager = PluginManagerFactory.createPluginManager();
addCustomUIPlugins(pluginManager);
File baseDir = VaadinService.getCurrent().getBaseDirectory();
File builtin = new File(baseDir, "WEB-INF/lib/annis-visualizers-"
+ VersionInfo.getReleaseName() + ".jar");
if(builtin.canRead()) {
pluginManager.addPluginsFrom(builtin.toURI());
log.info("added built-in plugins from {}", builtin.getPath());
} else {
log.warn("could not find built-in plugin file {}", builtin.getPath());
}
File basicPlugins = new File(baseDir, "WEB-INF/plugins");
if (basicPlugins.isDirectory())
{
pluginManager.addPluginsFrom(basicPlugins.toURI());
log.info("added plugins from {}", basicPlugins.getPath());
}
String globalPlugins = System.getenv("ANNIS_PLUGINS");
if (globalPlugins != null)
{
pluginManager.addPluginsFrom(new File(globalPlugins).toURI());
log.info("added plugins from {}", globalPlugins);
}
StringBuilder listOfPlugins = new StringBuilder();
listOfPlugins.append("loaded plugins:\n");
PluginManagerUtil util = new PluginManagerUtil(pluginManager);
for (Plugin p : util.getPlugins())
{
listOfPlugins.append(p.getClass().getName()).append("\n");
}
log.info(listOfPlugins.toString());
Collection<VisualizerPlugin> visualizers = util.getPlugins(
VisualizerPlugin.class);
for (VisualizerPlugin vis : visualizers)
{
visualizerRegistry.put(vis.getShortName(), vis);
resourceAddedDate.put(vis.getShortName(), new Date());
}
}
private static void checkIfRemoteLoggedIn(VaadinRequest request)
{
// check if we are logged in using an external authentification mechanism
// like Schibboleth
String remoteUser = request.getRemoteUser();
if(remoteUser != null)
{
Helper.setUser(new AnnisUser(remoteUser, null, true));
}
}
/**
* Inject CSS into the UI.
* This function will not add multiple style-elements if the
* exact CSS string was already added.
* @param cssContent
*/
public void injectUniqueCSS(String cssContent)
{
injectUniqueCSS(cssContent, null);
}
/**
* Inject CSS into the UI.
* This function will not add multiple style-elements if the
* exact CSS string was already added.
* @param cssContent
* @param wrapperClass Name of the wrapper class (a CSS class that is applied to a parent element)
*/
public void injectUniqueCSS(String cssContent, String wrapperClass)
{
if(alreadyAddedCSS == null)
{
alreadyAddedCSS = new TreeSet<String>();
}
if(wrapperClass != null)
{
cssContent = wrapCSS(cssContent, wrapperClass);
}
String hashForCssContent = Hashing.md5().hashString(cssContent, Charsets.UTF_8).toString();
if(!alreadyAddedCSS.contains(hashForCssContent))
{
// CSSInject cssInject = new CSSInject(UI.getCurrent());
// cssInject.setStyles(cssContent);
Page.getCurrent().getStyles().add(cssContent);
alreadyAddedCSS.add(hashForCssContent);
}
}
private String wrapCSS(String cssContent, String wrapperClass)
{
try
{
String wrappedContent
= wrapperClass == null ? cssContent
: "." + wrapperClass + "{\n"
+ cssContent
+ "\n}";
File tmpFile = File.createTempFile("annis-stylesheet", ".scss");
Files.write(wrappedContent, tmpFile, Charsets.UTF_8);
ScssStylesheet styleSheet = ScssStylesheet.get(tmpFile.getCanonicalPath());
styleSheet.compile();
return styleSheet.printState();
}
catch (IOException ex)
{
log.error("IOException when compiling wrapped CSS", ex);
}
catch (Exception ex)
{
log.error("Could not compile wrapped CSS", ex);
}
return null;
}
@Override
public PluginManager getPluginManager()
{
if (pluginManager == null)
{
initPlugins();
}
return pluginManager;
}
@Override
public VisualizerPlugin getVisualizer(String shortName)
{
return visualizerRegistry.get(shortName);
}
public ObjectMapper getJsonMapper()
{
if(jsonMapper == null)
{
jsonMapper = new ObjectMapper();
// configure json object mapper
AnnotationIntrospector introspector = new JaxbAnnotationIntrospector();
jsonMapper.setAnnotationIntrospector(introspector);
// the json should be human readable
jsonMapper.configure(SerializationConfig.Feature.INDENT_OUTPUT,
true);
jsonMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES , false);
}
return jsonMapper;
}
private static class RemoteUserRequestHandler implements RequestHandler
{
@Override
public boolean handleRequest(VaadinSession session, VaadinRequest request,
VaadinResponse response) throws IOException
{
checkIfRemoteLoggedIn(request);
// we never write any information in this handler
return false;
}
}
public EventBus getLoginDataLostBus()
{
if(loginDataLostBus == null)
{
loginDataLostBus = new EventBus();
}
return loginDataLostBus;
}
}