package org.ovirt.engine.core.branding;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import org.ovirt.engine.core.utils.servlet.LocaleFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents the components of an available theme. There are
* several things that are important to a theme:
* <ul>
* <li>The path to the theme</li>
* <li>The name of the style sheet associated with the theme</li>
* </ul>
*/
public class BrandingTheme {
/**
* The logger.
*/
private static final Logger log = LoggerFactory.getLogger(BrandingTheme.class);
/**
* The key for the messages resource bundle name.
*/
private static final String MESSAGES_KEY = "messages"; //$NON-NLS-1$
/**
* The key for the cascading resources bundle name.
*/
private static final String RESOURCES_KEY = "resources"; //$NON-NLS-1$
/**
* The suffix of properties file name.
*/
private static final String PROPERTIES_FILE_SUFFIX = ".properties"; //$NON-NLS-1$
/**
* The name of the branding properties file.
*/
private static final String BRANDING_PROPERTIES_NAME = "branding" + PROPERTIES_FILE_SUFFIX; //$NON-NLS-1$
/**
* The key to use to read the branding version.
*/
private static final String VERSION_KEY = "version"; //$NON-NLS-1$
/**
* The key used to read the welcome page template.
*/
private static final String TEMPLATE_KEY = "welcome"; //$NON-NLS-1$
/**
* The key used to determine if this template should completely replace the template build from all
* previously processed themes.
*/
private static final String REPLACE_TEMPLATE_KEY = "welcome_replace"; //$NON-NLS-1$
/**
* Property suffix for cascading resources file.
*/
private static final String FILE_SUFFIX = ".file"; //$NON-NLS-1$
/**
* Property suffix for cascading resources contentType.
*/
private static final String CONTENT_TYPE_SUFFIX = ".contentType"; //$NON-NLS-1$
/**
* Post fix for denoting css files.
*/
private static final String CSS_POST_FIX = "_css"; //$NON-NLS-1$
private static final String[] TEMPLATE_REPLACE_VALUES = {"true", "false"}; //$NON-NLS-1$ //$NON-NLS-2$
/**
* The properties associated with the branding theme.
*/
private final Properties brandingProperties = new Properties();
/**
* The path to the branding directory.
*/
private final String path;
/**
* The actual file path.
*/
private final String filePath;
/**
* The version of the branding theme we are supposed to be.
*/
private final int supportedBrandingVersion;
/**
* Availability flag.
*/
private boolean available;
/**
* Constructor.
* @param brandingPath The path to the theme
* @param brandingRootPath The root of the path to the branding theme,
* @param brandingVersion The version to load, if the version don't match the load will fail.
*/
public BrandingTheme(final String brandingPath, final File brandingRootPath, final int brandingVersion) {
path = brandingPath.substring(brandingRootPath.getAbsolutePath().length());
filePath = brandingPath;
supportedBrandingVersion = brandingVersion;
}
/**
* Load the branding theme based on the passed in paths.
* @return {@code true} if successfully loaded, {@code false} otherwise.
*/
public boolean load() {
final String propertiesFileName = filePath + "/" + BRANDING_PROPERTIES_NAME; //$NON-NLS-1$
available = false;
try (FileInputStream propertiesFile = new FileInputStream(propertiesFileName)) {
brandingProperties.load(propertiesFile);
available = supportedBrandingVersion == getVersion(brandingProperties);
if (!available) {
log.warn("Unable to load branding theme, mismatched version '{}' wanted version '{}'", //$NON-NLS-1$
getVersion(brandingProperties),
supportedBrandingVersion);
} else {
available = verifyPropertyValues(brandingProperties);
if (!available) {
log.warn("Unable to load branding theme, property value verification failed"); //$NON-NLS-1$
}
}
} catch (IOException e) {
// Unable to load properties file, disable theme.
log.warn("Unable to load properties file for theme located here '{}'", //$NON-NLS-1$
propertiesFileName,
e);
}
return available;
}
/**
* Verify that all required property values are valid.
* @param properties The {@code Properties} object to check.
*/
private boolean verifyPropertyValues(final Properties properties) {
boolean result = true;
if (brandingProperties.getProperty(REPLACE_TEMPLATE_KEY) != null &&
!Arrays.asList(TEMPLATE_REPLACE_VALUES).contains(
brandingProperties.getProperty(REPLACE_TEMPLATE_KEY).toLowerCase())) {
log.warn("'{}' value is not true or false", REPLACE_TEMPLATE_KEY); //$NON-NLS-1$
result = false;
}
return result;
}
/**
* Getter for the style path.
* @return A {@code String} containing the path.
*/
public String getPath() {
return path;
}
/**
* Get the version of the branding read.
* @param properties The {@code Properties} to use to get the version.
* @return The version of the branding theme read as an {@code integer}, 0 if we are unable to read
* the version.
*/
public int getVersion(final Properties properties) {
int result = 0;
try {
result = Integer.parseInt(properties.getProperty(VERSION_KEY));
} catch (NumberFormatException nfe) {
// Do nothing, not a valid version, return 0.
}
return result;
}
/**
* Get the css resources for this theme for this {@code ApplicationType}.
* @param applicationName The application name for which to get the css resources
* @return A {@code List} of filenames
*/
public List<String> getThemeStylesheets(String applicationName) {
List<String> ret = null;
final String cssFiles = brandingProperties.getProperty(applicationName + CSS_POST_FIX);
if (cssFiles == null) {
log.warn("Theme '{}' has no property defined for key '{}'", //$NON-NLS-1$
this.getPath(), applicationName + CSS_POST_FIX);
}
else {
// comma-delimited list
ret = Arrays.asList(cssFiles.split("\\s*,\\s*")); //$NON-NLS-1$
}
return ret;
}
/**
* Get the theme messages resource bundle for the US locale.
* @return A {@code ResourceBundle} containing messages.
*/
public List<ResourceBundle> getMessagesBundle() {
// Default to US Locale.
return getMessagesBundle(LocaleFilter.DEFAULT_LOCALE);
}
/**
* Get the theme messages resource bundle.
* @param locale the locale to load the bundle for.
* @return A {@code ResourceBundle} containing messages.
*/
public List<ResourceBundle> getMessagesBundle(final Locale locale) {
return getBundle(MESSAGES_KEY, locale);
}
/**
* Get the theme cascading resources bundle.
* @return A {@code ResourceBundle} containing resource paths.
*/
public ResourceBundle getResourcesBundle() {
List<ResourceBundle> bundleList = getBundle(RESOURCES_KEY, LocaleFilter.DEFAULT_LOCALE);
if (bundleList.size() >= 1) {
return bundleList.get(0);
}
throw new MissingResourceException("can't load resources bundle", null, null); //$NON-NLS-1$
}
/**
* Load the Java resource bundle associated with the passed in Locale and name.
* @param name The name of the {@code ResourceBundle} file.
* @param locale The locale to load.
* @return A {@code ResourceBundle} containing the resources.
*/
private List<ResourceBundle> getBundle(String name, Locale locale) {
List<ResourceBundle> result = new ArrayList<>();
String lastProcessedBundle = null;
try {
File themeDirectory = new File(filePath);
URLClassLoader urlLoader = new URLClassLoader(
new URL[] {
themeDirectory.toURI().toURL() });
final String messageFileNames = brandingProperties.getProperty(name);
if (messageFileNames != null) {
//The values can be a comma separated list of file names, split them and load each of them.
for (String fileName: messageFileNames.split(",")) {
fileName = lastProcessedBundle = fileName.trim();
String bundleName = fileName.lastIndexOf(PROPERTIES_FILE_SUFFIX) != -1
? fileName.substring(0, fileName.lastIndexOf(PROPERTIES_FILE_SUFFIX))
: messageFileNames;
result.add(ResourceBundle.getBundle(bundleName, locale, urlLoader));
}
} else {
log.warn("Theme '{}' has no property defined for key '{}'", //$NON-NLS-1$
this.getPath(),
name);
}
} catch (IOException e) {
// Unable to load messages resource bundle.
log.warn("Unable to read resources resource bundle, returning null", e); //$NON-NLS-1$
} catch (MissingResourceException mre) {
log.warn("Theme '{}' is missing ResourceBundle '{}'", //$NON-NLS-1$
this.getPath(),
lastProcessedBundle);
}
return result;
}
public boolean shouldReplaceWelcomePageSectionTemplate() {
return Boolean.valueOf(brandingProperties.getProperty(REPLACE_TEMPLATE_KEY));
}
/**
* Return the raw welcome template as a string.
* @return The raw template string.
*/
public String getWelcomePageSectionTemplate() {
String result = "";
try {
final String templateFileName = filePath + "/" + brandingProperties.getProperty(TEMPLATE_KEY); //$NON-NLS-1$
result = readWelcomeTemplateFile(templateFileName);
} catch (IOException ioe) {
log.error("Unable to load welcome template", ioe); //$NON-NLS-1$
} catch (NullPointerException e) {
log.error("Unable to locate welcome template key in branding properties", e); //$NON-NLS-1$
}
return result;
}
/**
* Read the welcome page template file. The template is standard HTML format, but with one difference.
* If a line starts with '#' it is considered a comment and will not end up in the output.
* @param fileName The name of the file to read.
* @return The contents of the file as a string.
* @throws IOException if unable to read the template file.
*/
private String readWelcomeTemplateFile(final String fileName) throws IOException {
StringBuilder templateBuilder = new StringBuilder();
try (
InputStream in = new FileInputStream(fileName);
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
BufferedReader bufferedReader= new BufferedReader(reader);
){
String currentLine;
while ((currentLine = bufferedReader.readLine()) != null) {
if (!currentLine.startsWith("#")) { // # is comment.
templateBuilder.append(currentLine);
templateBuilder.append("\n"); //$NON-NLS-1$
}
}
}
return templateBuilder.toString();
}
/**
* Look for the resource in this theme, and return the path to it if it exists.
* Return null if the resource isn't found in the theme.
* @param resourceName The name of the resource.
* @return resource path, or null if no resource found in this theme
*/
public CascadingResource getCascadingResource(String resourceName) {
if (resourceName == null) {
return null;
}
try {
String resourceFile = getResourcesBundle().getString(resourceName + FILE_SUFFIX);
File file = new File(filePath + "/" + resourceFile); //$NON-NLS-1$
if (file.exists() && file.canRead()) {
// ok, good, we have a file. was a contentType specified?
String contentType = null;
try {
contentType = getResourcesBundle().getString(resourceName + CONTENT_TYPE_SUFFIX);
} catch (MissingResourceException mre) {
// no-op -- contentType is optional, no big deal
}
return new CascadingResource(file, contentType);
}
} catch (MissingResourceException mre) {
// no-op -- this theme just doesn't have this resource. will try the next lowest theme.
}
return null;
}
@Override
public String toString() {
return "Path to theme: " + getPath() + ", User portal css: " //$NON-NLS-1$ //$NON-NLS-2$
+ getThemeStylesheets("userportal") + ", Web admin css: " //$NON-NLS-1$ //$NON-NLS-2$
+ getThemeStylesheets("webadmin") + ", Welcome page css: " //$NON-NLS-1$ //$NON-NLS-2$
+ getThemeStylesheets("welcome"); //$NON-NLS-1$
}
}