blob: c52e1f4d843bed4d17dd006562804ca3db5a278e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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.apache.geode.session.tests;
import static org.apache.geode.test.util.ResourceUtils.getResource;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Properties;
import java.util.function.IntSupplier;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.Logger;
import org.codehaus.cargo.container.installer.Installer;
import org.codehaus.cargo.container.installer.ZipURLInstaller;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.management.internal.configuration.utils.ZipUtils;
/**
* Base class for handling downloading and configuring J2EE containers.
*
* This class contains common logic for downloading and configuring J2EE containers with cargo, and
* some common methods for applying geode session replication configuration to those containers.
*
* Subclasses provide installation of specific containers.
*/
public abstract class ContainerInstall {
private final IntSupplier portSupplier;
static final Logger logger = LogService.getLogger();
static final String TMP_DIR = createTempDir();
static final String GEODE_BUILD_HOME = System.getenv("GEODE_HOME");
static final String GEODE_BUILD_HOME_LIB = GEODE_BUILD_HOME + "/lib/";
private static final String DEFAULT_INSTALL_DIR = TMP_DIR + "/cargo_containers/";
private static final String DEFAULT_MODULE_EXTRACTION_DIR = TMP_DIR + "/cargo_modules/";
static final String DEFAULT_MODULE_LOCATION = GEODE_BUILD_HOME + "/tools/Modules/";
protected IntSupplier portSupplier() {
return portSupplier;
}
private final ConnectionType connType;
private final String installPath;
private final String modulePath;
private final String warFilePath;
private final String defaultLocatorAddress;
private int defaultLocatorPort;
/**
* Represents the type of connection used in this installation
*
* Supports either PEER_TO_PEER or CLIENT_SERVER. Also containers several useful strings needed to
* identify XML files or connection types when setting up containers.
*/
public enum ConnectionType {
PEER_TO_PEER("peer-to-peer", "cache-peer.xml", false, false),
CLIENT_SERVER("client-server", "cache-client.xml", false, true),
CACHING_CLIENT_SERVER("client-server", "cache-client.xml", true, true);
private final String name;
private final String cacheXMLFileName;
private final boolean enableLocalCache;
private final boolean isClientServer;
ConnectionType(String name, String cacheXMLFileName, boolean enableLocalCache,
boolean isClientServer) {
this.name = name;
this.cacheXMLFileName = cacheXMLFileName;
this.enableLocalCache = enableLocalCache;
this.isClientServer = isClientServer;
}
public String getName() {
return name;
}
public String getCacheXMLFileName() {
return cacheXMLFileName;
}
public boolean enableLocalCache() {
return enableLocalCache;
}
public boolean isClientServer() {
return isClientServer;
}
}
public ContainerInstall(String name, String downloadURL, ConnectionType connectionType,
String moduleName, IntSupplier portSupplier) throws IOException {
this(name, downloadURL, connectionType, moduleName, DEFAULT_MODULE_LOCATION, portSupplier);
}
/**
* @param name used to name install directory
* @param connType Enum representing the connection type of this installation (either client
* server or peer to peer)
* @param moduleName The module name of the installation being setup (i.e. tomcat, appserver,
* etc.)
*/
public ContainerInstall(String name, String downloadURL, ConnectionType connType,
String moduleName, String geodeModuleLocation, IntSupplier portSupplier) throws IOException {
this.connType = connType;
this.portSupplier = portSupplier;
String installDir = DEFAULT_INSTALL_DIR + name;
clearPreviousInstall(installDir);
String resource = getResource(getClass(), "/" + downloadURL).getPath();
URL url = Paths.get(resource).toUri().toURL();
logger.info("Installing container from URL " + url);
// Optional step to install the container from a URL pointing to its distribution
Installer installer =
new ZipURLInstaller(url, TMP_DIR + "/downloads", installDir);
installer.install();
// Set install home
installPath = installer.getHome();
// Find and extract the module path
modulePath = findAndExtractModule(geodeModuleLocation, moduleName);
logger.info("Extracted module " + moduleName + " to " + modulePath);
// Find the session testing war path
warFilePath = findSessionTestingWar();
// Default locator
defaultLocatorPort = 8080;
defaultLocatorAddress = "localhost";
logger.info("Installed container into " + getHome());
}
ServerContainer generateContainer(String containerDescriptors) throws IOException {
return generateContainer(null, containerDescriptors);
}
/**
* Cleans up the installation by deleting the extracted module and downloaded installation folders
*/
private void clearPreviousInstall(String installDir) throws IOException {
File installFolder = new File(installDir);
// Remove installs from previous runs in the same folder
if (installFolder.exists()) {
logger.info("Deleting previous install folder " + installFolder.getAbsolutePath());
FileUtils.deleteDirectory(installFolder);
}
}
void setDefaultLocatorPort(int port) {
defaultLocatorPort = port;
}
/**
* Whether the installation is client server
*
* Since an installation can only be client server or peer to peer there is no need for a function
* which checks for a peer to peer installation (just check if not client server).
*/
boolean isClientServer() {
return connType.isClientServer();
}
/**
* Where the installation is located
*/
public String getHome() {
return installPath;
}
/**
* Where the module is located
*
* The module contains jars needed for geode session setup as well as default templates for some
* needed XML files.
*/
String getModulePath() {
return modulePath;
}
/**
* The path to the session testing WAR file
*/
String getWarFilePath() {
return warFilePath;
}
/**
* @return The enum {@link #connType} which represents the type of connection for this
* installation
*/
ConnectionType getConnectionType() {
return connType;
}
/**
* Gets the {@link #defaultLocatorAddress}
*
* This is the address that a container uses by default. Containers themselves can have their own
* personal locator address, but will default to this address unless specifically set.
*/
String getDefaultLocatorAddress() {
return defaultLocatorAddress;
}
/**
* Gets the {@link #defaultLocatorPort}
*
* This is the port that a container uses by default. Containers themselves can have their own
* personal locator port, but will default to this port unless specifically set.
*/
int getDefaultLocatorPort() {
return defaultLocatorPort;
}
/**
* Gets the cache XML file to use by default for this installation
*/
File getCacheXMLFile() {
return new File(modulePath + "/conf/" + getConnectionType().getCacheXMLFileName());
}
/**
* Cargo specific string to identify the container with
*/
public abstract String getInstallId();
/**
* A human readable description of the installation
*/
public abstract String getInstallDescription();
/**
* Get the session manager class to use
*/
public abstract String getContextSessionManagerClass();
/**
* Generates a {@link ServerContainer} from the given {@link ContainerInstall}
*
* @param containerDescriptors Additional descriptors used to identify a container
*/
public abstract ServerContainer generateContainer(File containerConfigHome,
String containerDescriptors) throws IOException;
/**
* Get the path to the session testing war by walking up directories to the correct folder.
*
* NOTE::This walks into the extensions folder and then uses a hardcoded path from there making it
* very unreliable if things are moved.
*/
private static String findSessionTestingWar() {
// Start out searching directory above current
String curPath = "../";
// Looking for extensions folder
final String warModuleDirName = "extensions";
File warModuleDir = null;
// While directory searching for is not found
while (warModuleDir == null) {
// Try to find the find the directory in the current directory
File[] files = new File(curPath).listFiles();
for (File file : files) {
if (file.isDirectory() && file.getName().equals(warModuleDirName)) {
warModuleDir = file;
break;
}
}
// Keep moving up until you find it
curPath += "../";
}
// Return path to extensions plus hardcoded path from there to the WAR
return warModuleDir.getAbsolutePath()
+ "/session-testing-war/build/libs/session-testing-war.war";
}
/**
* Finds and extracts the geode module associated with the specified module.
*
* @param moduleName The module name (i.e. tomcat, appserver, etc.) of the module that should be
* extract. Used as a search parameter to find the module archive.
* @return The path to the non-archive (extracted) version of the module files
*/
private static String findAndExtractModule(String geodeModuleLocation, String moduleName)
throws IOException {
File modulesDir = new File(geodeModuleLocation);
logger.info("Trying to access build dir " + modulesDir);
// Search directory for tomcat module folder/zip
boolean archive = false;
File modulePath = null;
for (File file : modulesDir.listFiles()) {
if (file.getName().toLowerCase().contains(moduleName)) {
modulePath = file;
archive = !file.isDirectory();
if (!archive) {
break;
}
}
}
assertThat(modulePath).describedAs("module path").isNotNull();
String extractedModulePath =
modulePath.getName().substring(0, modulePath.getName().length() - 4);
// Get the name of the new module folder within the extraction directory
File newModuleFolder = new File(DEFAULT_MODULE_EXTRACTION_DIR + extractedModulePath);
// Remove any previous module folders extracted here
if (newModuleFolder.exists()) {
logger.info("Deleting previous modules directory " + newModuleFolder.getAbsolutePath());
FileUtils.deleteDirectory(newModuleFolder);
}
// Unzip if it is a zip file
if (archive) {
if (!FilenameUtils.getExtension(modulePath.getAbsolutePath()).equals("zip")) {
throw new IOException("Bad module archive " + modulePath);
}
// Extract folder to location if not already there
if (!newModuleFolder.exists()) {
ZipUtils.unzip(modulePath.getAbsolutePath(), newModuleFolder.getAbsolutePath());
}
modulePath = newModuleFolder;
}
// No module found within directory throw IOException
if (modulePath == null) {
throw new IOException("No module found in " + modulesDir);
}
return modulePath.getAbsolutePath();
}
/**
* Edits the specified property within the given property file
*
* @param filePath path to the property file
* @param propertyName property name to edit
* @param propertyValue new property value
* @param append whether or not to append the given property value. If true appends the given
* property value the current value. If false, replaces the current property value with the
* given property value
*/
static void editPropertyFile(String filePath, String propertyName, String propertyValue,
boolean append) throws Exception {
FileInputStream input = new FileInputStream(filePath);
Properties properties = new Properties();
properties.load(input);
String val;
if (append) {
val = properties.getProperty(propertyName) + propertyValue;
} else {
val = propertyValue;
}
properties.setProperty(propertyName, val);
properties.store(new FileOutputStream(filePath), null);
logger.info("Modified container Property file " + filePath);
}
static void editXMLFile(String XMLPath, String tagId, String tagName,
String parentTagName, HashMap<String, String> attributes) {
editXMLFile(XMLPath, tagId, tagName, tagName, parentTagName, attributes, false);
}
static void editXMLFile(String XMLPath, String tagName, String parentTagName,
HashMap<String, String> attributes, boolean writeOnSimilarAttributeNames) {
editXMLFile(XMLPath, null, tagName, tagName, parentTagName, attributes,
writeOnSimilarAttributeNames);
}
/**
* Edit the given xml file
*
* Uses {@link #findNodeWithAttribute(Document, String, String, String)},
* {@link #rewriteNodeAttributes(Node, HashMap)},
* {@link #nodeHasExactAttributes(Node, HashMap, boolean)} to edit the required parts of the XML
* file.
*
* @param XMLPath The path to the xml file to edit
* @param tagId The id of tag to edit. If null, then this method will add a new xml element,
* unless writeOnSimilarAttributeNames is set to true.
* @param tagName The name of the xml element to edit
* @param replacementTagName The new name of the XML attribute that is being edited
* @param parentTagName The parent element of the element we should edit
* @param attributes the xml attributes for the element to edit
* @param writeOnSimilarAttributeNames If true, find an existing element with the same set of
* attributes as the attributes parameter, and modifies the attributes of that element,
* rather than adding a new element. If false, create a new XML element (unless tagId is
* not null).
*/
private static void editXMLFile(String XMLPath, String tagId, String tagName,
String replacementTagName, String parentTagName, HashMap<String, String> attributes,
boolean writeOnSimilarAttributeNames) {
try {
// Get XML file to edit
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document doc = docBuilder.parse(XMLPath);
Node node = null;
// Get node with specified tagId
if (tagId != null) {
node = findNodeWithAttribute(doc, tagName, "id", tagId);
}
// If writing on similar attributes then search by tag name
else if (writeOnSimilarAttributeNames) {
// Get all the nodes with the given tag name
NodeList nodes = doc.getElementsByTagName(tagName);
for (int i = 0; i < nodes.getLength(); i++) {
Node n = nodes.item(i);
// If the node being iterated across has the exact attributes then it is the one that
// should be edited
if (nodeHasExactAttributes(n, attributes, false)) {
node = n;
break;
}
}
}
// If a node if found
if (node != null) {
doc.renameNode(node, null, replacementTagName);
// Rewrite the node attributes
rewriteNodeAttributes(node, attributes);
// Write the tagId so that it can be found easier next time
if (tagId != null)
((Element) node).setAttribute("id", tagId);
}
// No node found creates new element under the parent tag passed in
else {
Element e = doc.createElement(replacementTagName);
// Set id attribute
if (tagId != null) {
e.setAttribute("id", tagId);
}
// Set other attributes
for (String key : attributes.keySet()) {
e.setAttribute(key, attributes.get(key));
}
// Add it as a child of the tag for the file
doc.getElementsByTagName(parentTagName).item(0).appendChild(e);
}
// Write updated XML file
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
DOMSource source = new DOMSource(doc);
StreamResult result = new StreamResult(new File(XMLPath));
transformer.transform(source, result);
logger.info("Modified container XML file " + XMLPath);
} catch (Exception e) {
throw new RuntimeException("Unable to edit XML file", e);
}
}
/**
* Finds the node in the given document with the given name and attribute
*
* @param doc XML document to search for the node
* @param nodeName The name of the node to search for
* @param name The name of the attribute that the node should contain
* @param value The value of the node's given attribute
* @return Node with the given name, attribute, and attribute value
*/
private static Node findNodeWithAttribute(Document doc, String nodeName, String name,
String value) {
// Get all nodes with given name
NodeList nodes = doc.getElementsByTagName(nodeName);
if (nodes == null) {
return null;
}
// Find and return the first node that has the given attribute
for (int i = 0; i < nodes.getLength(); i++) {
Node node = nodes.item(i);
Node nodeAttr = node.getAttributes().getNamedItem(name);
if (nodeAttr != null && nodeAttr.getTextContent().equals(value)) {
return node;
}
}
return null;
}
/**
* Replaces the node's attributes with the attributes in the given hashmap
*
* @param node XML node that should be edited
* @param attributes HashMap of strings representing the attributes of a node (key = value)
* @return The given node with ONLY the given attributes
*/
private static Node rewriteNodeAttributes(Node node, HashMap<String, String> attributes) {
NamedNodeMap nodeAttrs = node.getAttributes();
// Remove all previous attributes
while (nodeAttrs.getLength() > 0) {
nodeAttrs.removeNamedItem(nodeAttrs.item(0).getNodeName());
}
// Set to new attributes
for (String key : attributes.keySet()) {
((Element) node).setAttribute(key, attributes.get(key));
}
return node;
}
/**
* Checks to see whether the given XML node has the exact attributes given in the attributes
* hashmap
*
* @param checkSimilarValues If true, will also check to make sure that the given node's
* attributes also have the exact same values as the ones given in the attributes HashMap.
* @return True if the node has only the attributes the are given by the HashMap (no more and no
* less attributes). If {@param checkSimilarValues} is true then only returns true if the
* node shares attributes with the given attribute list exactly.
*/
private static boolean nodeHasExactAttributes(Node node, HashMap<String, String> attributes,
boolean checkSimilarValues) {
NamedNodeMap nodeAttrs = node.getAttributes();
// Check to make sure the node has all attribute fields
for (String key : attributes.keySet()) {
Node attr = nodeAttrs.getNamedItem(key);
if (attr == null
|| checkSimilarValues && !attr.getTextContent().equals(attributes.get(key))) {
return false;
}
}
// Check to make sure the node does not have more than the attribute fields
for (int i = 0; i < nodeAttrs.getLength(); i++) {
String attr = nodeAttrs.item(i).getNodeName();
if (attributes.get(attr) == null || checkSimilarValues
&& !attributes.get(attr).equals(nodeAttrs.item(i).getTextContent())) {
return false;
}
}
return true;
}
private static String createTempDir() {
try {
return Files.createTempDirectory("geode_container_install")
.toAbsolutePath()
.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}