/**
* $RCSfile$
* $Revision: 11388 $
* $Date: 2009-11-08 18:26:55 -0600 (Sun, 08 Nov 2009) $
*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* 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.jivesoftware.admin;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.clearspace.ClearspaceManager;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.PropertyEventDispatcher;
import org.jivesoftware.util.PropertyEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A model for admin tab and sidebar info. This class loads in XML definitions of the
* data and produces an in-memory model.<p>
*
* This class loads its data from the <tt>admin-sidebar.xml</tt> file which is assumed
* to be in the main application jar file. In addition, it will load files from
* <tt>META-INF/admin-sidebar.xml</tt> if they're found. This allows developers to
* extend the functionality of the admin console to provide more options. See the main
* <tt>admin-sidebar.xml</tt> file for documentation of its format.
*/
public class AdminConsole {
private static final Logger Log = LoggerFactory.getLogger(AdminConsole.class);
private static Element coreModel;
private static Map<String,Element> overrideModels;
private static Element generatedModel;
static {
overrideModels = new LinkedHashMap<String,Element>();
load();
// Detect when a new auth provider class is set to ClearspaceAuthProvider
// then rebuild the model to add the Clearspace tab
// This is to add the tab after Openfire setup
PropertyEventListener propListener = new PropertyEventListener() {
public void propertySet(String property, Map params) {
if ("provider.auth.className".equals(property)) {
String value = (String) params.get("value");
if ("org.jivesoftware.openfire.clearspace.ClearspaceAuthProvider".equals(value)) {
rebuildModel();
}
}
}
public void propertyDeleted(String property, Map params) {
//Ignore
}
public void xmlPropertySet(String property, Map params) {
//Ignore
}
public void xmlPropertyDeleted(String property, Map params) {
//Ignore
}
};
PropertyEventDispatcher.addListener(propListener);
}
/** Not instantiatable */
private AdminConsole() {
}
/**
* Adds XML stream to the tabs/sidebar model.
*
* @param name the name.
* @param in the XML input stream.
* @throws Exception if an error occurs when parsing the XML or adding it to the model.
*/
public static void addModel(String name, InputStream in) throws Exception {
SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(in);
addModel(name, (Element)doc.selectSingleNode("/adminconsole"));
}
/**
* Adds an <adminconsole> Element to the tabs/sidebar model.
*
* @param name the name.
* @param element the Element
* @throws Exception if an error occurs.
*/
public static void addModel(String name, Element element) throws Exception {
overrideModels.put(name, element);
rebuildModel();
}
/**
* Removes an <adminconsole> Element from the tabs/sidebar model.
*
* @param name the name.
*/
public static void removeModel(String name) {
overrideModels.remove(name);
rebuildModel();
}
/**
* Returns the name of the application.
*
* @return the name of the application.
*/
public static synchronized String getAppName() {
Element appName = (Element)generatedModel.selectSingleNode("//adminconsole/global/appname");
if (appName != null) {
String pluginName = appName.attributeValue("plugin");
return getAdminText(appName.getText(), pluginName);
}
else {
return null;
}
}
/**
* Returns the URL of the main logo image for the admin console.
*
* @return the logo image.
*/
public static synchronized String getLogoImage() {
Element globalLogoImage = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/logo-image");
if (globalLogoImage != null) {
String pluginName = globalLogoImage.attributeValue("plugin");
return getAdminText(globalLogoImage.getText(), pluginName);
}
else {
return null;
}
}
/**
* Returns the URL of the login image for the admin console.
*
* @return the login image.
*/
public static synchronized String getLoginLogoImage() {
Element globalLoginLogoImage = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/login-image");
if (globalLoginLogoImage != null) {
String pluginName = globalLoginLogoImage.attributeValue("plugin");
return getAdminText(globalLoginLogoImage.getText(), pluginName);
}
else {
return null;
}
}
/**
* Returns the version string displayed in the admin console.
*
* @return the version string.
*/
public static synchronized String getVersionString() {
Element globalVersion = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/version");
if (globalVersion != null) {
String pluginName = globalVersion.attributeValue("plugin");
return getAdminText(globalVersion.getText(), pluginName);
}
else {
// Default to the Openfire version if none has been provided via XML.
XMPPServer xmppServer = XMPPServer.getInstance();
return xmppServer.getServerInfo().getVersion().getVersionString();
}
}
/**
* Returns the model. The model should be considered read-only.
*
* @return the model.
*/
public static synchronized Element getModel() {
return generatedModel;
}
/**
* Convenience method to select an element from the model by its ID. If an
* element with a matching ID is not found, <tt>null</tt> will be returned.
*
* @param id the ID.
* @return the element.
*/
public static synchronized Element getElemnetByID(String id) {
return (Element)generatedModel.selectSingleNode("//*[@id='" + id + "']");
}
/**
* Returns a text element for the admin console, applying the appropriate locale.
* Internationalization logic will only be applied if the String is specially encoded
* in the format "${key.name}". If it is, the String is pulled from the resource bundle.
* If the pluginName is not <tt>null</tt>, the plugin's resource bundle will be used
* to look up the key.
*
* @param string the String.
* @param pluginName the name of the plugin that the i18n String can be found in,
* or <tt>null</tt> if the standard Openfire resource bundle should be used.
* @return the string, or if the string is encoded as an i18n key, the value from
* the appropriate resource bundle.
*/
public static String getAdminText(String string, String pluginName) {
if (string == null) {
return null;
}
// Look for the key symbol:
if (string.indexOf("${") == 0 && string.indexOf("}") == string.length()-1) {
return LocaleUtils.getLocalizedString(string.substring(2, string.length()-1), pluginName);
}
return string;
}
private static void load() {
// Load the core model as the admin-sidebar.xml file from the classpath.
InputStream in = ClassUtils.getResourceAsStream("/admin-sidebar.xml");
if (in == null) {
Log.error("Failed to load admin-sidebar.xml file from Openfire classes - admin "
+ "console will not work correctly.");
return;
}
try {
SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(in);
coreModel = (Element)doc.selectSingleNode("/adminconsole");
}
catch (Exception e) {
Log.error("Failure when parsing main admin-sidebar.xml file", e);
}
try {
in.close();
}
catch (Exception ignored) {
// Ignore.
}
// Load other admin-sidebar.xml files from the classpath
ClassLoader[] classLoaders = getClassLoaders();
for (ClassLoader classLoader : classLoaders) {
URL url = null;
try {
if (classLoader != null) {
Enumeration e = classLoader.getResources("/META-INF/admin-sidebar.xml");
while (e.hasMoreElements()) {
url = (URL) e.nextElement();
try {
in = url.openStream();
addModel("admin", in);
}
finally {
try {
if (in != null) {
in.close();
}
}
catch (Exception ignored) {
// Ignore.
}
}
}
}
}
catch (Exception e) {
String msg = "Failed to load admin-sidebar.xml";
if (url != null) {
msg += " from resource: " + url.toString();
}
Log.warn(msg, e);
}
}
rebuildModel();
}
/**
* Rebuilds the generated model.
*/
private static synchronized void rebuildModel() {
Document doc = DocumentFactory.getInstance().createDocument();
generatedModel = coreModel.createCopy();
doc.add(generatedModel);
// Add in all overrides.
for (Element element : overrideModels.values()) {
// See if global settings are overriden.
Element appName = (Element)element.selectSingleNode("//adminconsole/global/appname");
if (appName != null) {
Element existingAppName = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/appname");
existingAppName.setText(appName.getText());
if (appName.attributeValue("plugin") != null) {
existingAppName.addAttribute("plugin", appName.attributeValue("plugin"));
}
}
Element appLogoImage = (Element)element.selectSingleNode("//adminconsole/global/logo-image");
if (appLogoImage != null) {
Element existingLogoImage = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/logo-image");
existingLogoImage.setText(appLogoImage.getText());
if (appLogoImage.attributeValue("plugin") != null) {
existingLogoImage.addAttribute("plugin", appLogoImage.attributeValue("plugin"));
}
}
Element appLoginImage = (Element)element.selectSingleNode("//adminconsole/global/login-image");
if (appLoginImage != null) {
Element existingLoginImage = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/login-image");
existingLoginImage.setText(appLoginImage.getText());
if (appLoginImage.attributeValue("plugin") != null) {
existingLoginImage.addAttribute("plugin", appLoginImage.attributeValue("plugin"));
}
}
Element appVersion = (Element)element.selectSingleNode("//adminconsole/global/version");
if (appVersion != null) {
Element existingVersion = (Element)generatedModel.selectSingleNode(
"//adminconsole/global/version");
if (existingVersion != null) {
existingVersion.setText(appVersion.getText());
if (appVersion.attributeValue("plugin") != null) {
existingVersion.addAttribute("plugin", appVersion.attributeValue("plugin"));
}
}
else {
((Element)generatedModel.selectSingleNode(
"//adminconsole/global")).add(appVersion.createCopy());
}
}
// Tabs
for (Object o : element.selectNodes("//tab")) {
Element tab = (Element) o;
String id = tab.attributeValue("id");
Element existingTab = getElemnetByID(id);
// Simple case, there is no existing tab with the same id.
if (existingTab == null) {
// Make sure that the URL on the tab is set. If not, default to the
// url of the first item.
if (tab.attributeValue("url") == null) {
Element firstItem = (Element) tab.selectSingleNode(
"//item[@url]");
if (firstItem != null) {
tab.addAttribute("url", firstItem.attributeValue("url"));
}
}
generatedModel.add(tab.createCopy());
}
// More complex case -- a tab with the same id already exists.
// In this case, we have to overrite only the difference between
// the two elements.
else {
overrideTab(existingTab, tab);
}
}
}
// Special case: show a link to Clearspace admin console if it is integrated with
// Openfire.
if (ClearspaceManager.isEnabled()) {
Element clearspace = generatedModel.addElement("tab");
clearspace.addAttribute("id", "tab-clearspace");
clearspace.addAttribute("name", LocaleUtils.getLocalizedString("tab.tab-clearspace"));
clearspace.addAttribute("url", "clearspace-status.jsp");
clearspace.addAttribute("description", LocaleUtils.getLocalizedString("tab.tab-clearspace.descr"));
Element sidebar = clearspace.addElement("sidebar");
sidebar.addAttribute("id", "sidebar-clearspace");
sidebar.addAttribute("name", LocaleUtils.getLocalizedString("sidebar.sidebar-clearspace"));
Element statusItem = sidebar.addElement("item");
statusItem.addAttribute("id", "clearspace-status");
statusItem.addAttribute("name", LocaleUtils.getLocalizedString("sidebar.clearspace-status"));
statusItem.addAttribute("url", "clearspace-status.jsp");
statusItem.addAttribute("description", LocaleUtils.getLocalizedString("sidebar.clearspace-status.descr"));
Element adminItem = sidebar.addElement("item");
adminItem.addAttribute("id", "clearspace-admin");
adminItem.addAttribute("name", LocaleUtils.getLocalizedString("sidebar.clearspace-admin"));
adminItem.addAttribute("url", "clearspace-admin.jsp");
adminItem.addAttribute("description", LocaleUtils.getLocalizedString("sidebar.clearspace-admin.descr"));
}
}
private static void overrideTab(Element tab, Element overrideTab) {
// Override name, url, description.
if (overrideTab.attributeValue("name") != null) {
tab.addAttribute("name", overrideTab.attributeValue("name"));
}
if (overrideTab.attributeValue("url") != null) {
tab.addAttribute("url", overrideTab.attributeValue("url"));
}
if (overrideTab.attributeValue("description") != null) {
tab.addAttribute("description", overrideTab.attributeValue("description"));
}
if (overrideTab.attributeValue("plugin") != null) {
tab.addAttribute("plugin", overrideTab.attributeValue("plugin"));
}
// Override sidebar items.
for (Iterator i=overrideTab.elementIterator(); i.hasNext(); ) {
Element sidebar = (Element)i.next();
String id = sidebar.attributeValue("id");
Element existingSidebar = getElemnetByID(id);
// Simple case, there is no existing sidebar with the same id.
if (existingSidebar == null) {
tab.add(sidebar.createCopy());
}
// More complex case -- a sidebar with the same id already exists.
// In this case, we have to overrite only the difference between
// the two elements.
else {
overrideSidebar(existingSidebar, sidebar);
}
}
}
private static void overrideSidebar(Element sidebar, Element overrideSidebar) {
// Override name.
if (overrideSidebar.attributeValue("name") != null) {
sidebar.addAttribute("name", overrideSidebar.attributeValue("name"));
}
if (overrideSidebar.attributeValue("plugin") != null) {
sidebar.addAttribute("plugin", overrideSidebar.attributeValue("plugin"));
}
// Override entries.
for (Iterator i=overrideSidebar.elementIterator(); i.hasNext(); ) {
Element entry = (Element)i.next();
String id = entry.attributeValue("id");
Element existingEntry = getElemnetByID(id);
// Simple case, there is no existing sidebar with the same id.
if (existingEntry == null) {
sidebar.add(entry.createCopy());
}
// More complex case -- an entry with the same id already exists.
// In this case, we have to overrite only the difference between
// the two elements.
else {
overrideEntry(existingEntry, entry);
}
}
}
private static void overrideEntry(Element entry, Element overrideEntry) {
// Override name.
if (overrideEntry.attributeValue("name") != null) {
entry.addAttribute("name", overrideEntry.attributeValue("name"));
}
if (overrideEntry.attributeValue("url") != null) {
entry.addAttribute("url", overrideEntry.attributeValue("url"));
}
if (overrideEntry.attributeValue("description") != null) {
entry.addAttribute("description", overrideEntry.attributeValue("description"));
}
if (overrideEntry.attributeValue("plugin") != null) {
entry.addAttribute("plugin", overrideEntry.attributeValue("plugin"));
}
// Override any sidebars contained in the entry.
for (Iterator i=overrideEntry.elementIterator(); i.hasNext(); ) {
Element sidebar = (Element)i.next();
String id = sidebar.attributeValue("id");
Element existingSidebar = getElemnetByID(id);
// Simple case, there is no existing sidebar with the same id.
if (existingSidebar == null) {
entry.add(sidebar.createCopy());
}
// More complex case -- a sidebar with the same id already exists.
// In this case, we have to overrite only the difference between
// the two elements.
else {
overrideSidebar(existingSidebar, sidebar);
}
}
}
/**
* Returns an array of class loaders to load resources from.
*
* @return an array of class loaders to load resources from.
*/
private static ClassLoader[] getClassLoaders() {
ClassLoader[] classLoaders = new ClassLoader[3];
classLoaders[0] = AdminConsole.class.getClass().getClassLoader();
classLoaders[1] = Thread.currentThread().getContextClassLoader();
classLoaders[2] = ClassLoader.getSystemClassLoader();
return classLoaders;
}
}