/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.plugins.jboss.software;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathFactory;
import churchillobjects.rss4j.RssChannel;
import churchillobjects.rss4j.RssChannelItem;
import churchillobjects.rss4j.RssDocument;
import churchillobjects.rss4j.RssDublinCore;
import churchillobjects.rss4j.RssJbnDependency;
import churchillobjects.rss4j.RssJbnPatch;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.enterprise.server.plugin.pc.content.ContentProviderPackageDetails;
import org.rhq.enterprise.server.plugin.pc.content.ContentProviderPackageDetailsKey;
import org.rhq.enterprise.server.plugin.pc.content.PackageSyncReport;
/**
* Parses the contents of the JBoss RSS feed into the server's domain model.
*
* @author Jason Dobies
*/
public class RssFeedParser {
// Constants --------------------------------------------
private static final String JBOSS_AS4_PLUGIN_NAME = "JBossAS";
private static final String JBOSS_AS5_PLUGIN_NAME = "JBossAS5";
private static final String ARCHITECTURE = "noarch";
/**
* Corresponds to the resource type name defined in the JBossAS or JBossAS5 plugin.
*/
private static final String RESOURCE_TYPE_JBOSS_AS = "JBossAS Server";
/**
* Corresponds to the package type name defined in the JBoss AS plugin.
*/
private static final String PACKAGE_TYPE_CUMULATIVE_PATCH = "cumulativePatch";
private static final String RSS_SOFTWARE_TYPE_BUGFIX = "BUGFIX";
private static final String RSS_SOFTWARE_TYPE_SECURITY = "SECURITY";
private static final String RSS_SOFTWARE_TYPE_ENHANCEMENT = "ENHANCEMENT";
private static final String RSS_SOFTWARE_TYPE_DISTRIBUTION = "DISTRIBUTION";
private static final String DIST_STATUS_AVAILABLE = "AVAILABLE";
private static final String DIST_STATUS_OBSOLETE = "OBSOLETE";
private static final String DIST_STATUS_REMOVED = "REMOVED";
// Attributes --------------------------------------------
private final Log log = LogFactory.getLog(this.getClass());
// Public --------------------------------------------
public void parseResults(RssDocument feed, PackageSyncReport report,
Collection<ContentProviderPackageDetails> existingPackages) throws ParserConfigurationException {
// Used to determine if a package was already sent to the server or is new
Map<PackageDetailsKey, ContentProviderPackageDetails> existingPackageMap = unpack(existingPackages);
Enumeration repos = feed.channels();
// do setup in preparation for parsing the automated installation instructions
DocumentBuilderFactory xmlFact = DocumentBuilderFactory.newInstance();
xmlFact.setNamespaceAware(false);
DocumentBuilder builder = xmlFact.newDocumentBuilder();
XPath xpath = XPathFactory.newInstance().newXPath();
String instructionExpression = "/automatedInstallation/instructions/instructionSet";
while (repos.hasMoreElements()) {
RssChannel repo = (RssChannel) repos.nextElement();
Enumeration repoItems = repo.items();
while (repoItems.hasMoreElements()) {
RssChannelItem item = (RssChannelItem) repoItems.nextElement();
RssJbnPatch patch = item.getJbnPatch();
RssDublinCore dublinCore = item.getDublinCore();
// We need the data in these objects, so skip if either are null. I'm not sure this constitutes
// an error, but leaving the log message at warn for now.
if ((dublinCore == null) || (patch == null)) {
log.debug("Feed entry parsed data returned null. Skipping entry. Patch: " + patch
+ ", Dublin Core: " + dublinCore);
continue;
}
// If there are no products against which the software applies, punch out early
Collection products = patch.getProducts();
if ((products == null) || (products.size() == 0)) {
continue;
}
// First class properties
String packageName = dublinCore.getSubject();
String softwareType = patch.getType();
// Extra properties
String distributionStatus = patch.getDistributionStatus();
String jiraId = patch.getJira();
String downloadUrl = patch.getDownloadUrl();
String instructionCompatibilityVersion = patch.getInstructionCompatibilityVersion();
// If the distribution status indicates it's removed, don't do anything. Later in this method, if
// this package was known to the server, it will still be in the existing packages map and marked
// as deleted at the end. If the server didn't know about it, then nothing had to be done.
if (distributionStatus.equals(DIST_STATUS_REMOVED)) {
continue;
}
Configuration extraProperties = new Configuration();
extraProperties.put(new PropertySimple("jiraId", jiraId));
extraProperties.put(new PropertySimple("distributionStatus", distributionStatus));
extraProperties.put(new PropertySimple("downloadUrl", downloadUrl));
extraProperties.put(new PropertySimple("instructionCompatibilityVersion",
instructionCompatibilityVersion));
/* This will be refactored when we add support for the other types of data coming across in the feed,
such as product distributions and security/configuration advisories. For now, just leaving the
cumulative patch handling in here, but will clean up when the other types are supported.
jdobies, Jan 9, 2008
*/
// Technically, this is a check for installable patches. But for now, that's the same as a cumulative
// patch
if (softwareType.equals(RSS_SOFTWARE_TYPE_BUGFIX) && instructionCompatibilityVersion != null) {
String displayVersion = parseCumulativePatchVersion(packageName);
if (displayVersion == null) {
log.error("Could not parse version for package: " + packageName + ". Package skipped.");
continue;
}
if (patch.getSha256() == null) {
log.error("Could not parse SHA256 for package: " + packageName + ". Package skipped.");
continue;
}
String version = "[sha256=" + patch.getSha256() + "]";
ContentProviderPackageDetailsKey key = new ContentProviderPackageDetailsKey(packageName, version,
PACKAGE_TYPE_CUMULATIVE_PATCH, ARCHITECTURE, RESOURCE_TYPE_JBOSS_AS,
getPluginName(displayVersion));
// If this package is already known to the server, don't add it as a new package
// Remove from the map; entries still in the map will be returned as deleted packages
if (existingPackageMap.get(key) != null) {
existingPackageMap.remove(key);
continue;
}
ContentProviderPackageDetails packageDetails = new ContentProviderPackageDetails(key);
packageDetails.setClassification(softwareType);
packageDetails.setDisplayName(packageName);
packageDetails.setFileCreatedDate(dublinCore.getDate().getTime());
packageDetails.setFileName(patch.getFileName());
packageDetails.setFileSize(Long.parseLong(patch.getFileSize()));
packageDetails.setLicenseName(patch.getLicenseName());
packageDetails.setLicenseVersion(patch.getLicenseVersion());
packageDetails.setLocation(patch.getAutomatedDownloadUrl());
packageDetails.setMD5(patch.getMd5());
packageDetails.setSHA256(patch.getSha256());
packageDetails.setDisplayVersion(displayVersion);
packageDetails.setShortDescription(patch.getShortDescription());
packageDetails.setLongDescription(patch.getLongDescription());
if (patch.getAutomatedInstallation() != null) {
String instructions = patch.getAutomatedInstallation();
// JOPR-51, remove some of the xml elements which wrap the automated installation
// instructions but which aren't needed by JBPM
try {
Document document = builder.parse(new ByteArrayInputStream(instructions.getBytes()));
String choppedInstructions = xpath.evaluate(instructionExpression, document);
// The JBPM XML processor doesn't like whitespace at the beginning, so trim it.
// Bytes will be retrieved using platform encoding on the agent side, so use the same
// on the server side and assume they are identical.
packageDetails.setMetadata(choppedInstructions.trim().getBytes());
} catch (Exception e) {
log.error("Could not parse or set automated installation instructions for package: "
+ packageName);
continue;
}
}
packageDetails.setExtraProperties(extraProperties);
// For each product listed for the patch, add an entry for its resource version.
for (Object productObj : products) {
RssJbnDependency product = (RssJbnDependency) productObj;
// The CSP feed will include an optional JON version that maps up to how the resource
// version will be identified by RHQ. If this is specified, we'll want to use the mapped
// version instead.
String productVersion = product.getJonResourceVersion();
if (product.getProductVersion().equals("4.0.4")) {
productVersion = "4.0.4.GA";
}
if ((null == productVersion) || ("".equals(productVersion.trim()))) {
productVersion = product.getProductVersion();
}
packageDetails.addResourceVersion(productVersion);
}
report.addNewPackage(packageDetails);
}
}
}
// For each entry still in the map, we didn't find it again, so report it as deleted
for (ContentProviderPackageDetails pkg : existingPackageMap.values()) {
report.addDeletePackage(pkg);
}
}
// Private --------------------------------------------
/**
* Translates the set of packages into a map, using the package key object as the map's key entry.
*
* @param existingPackages packages sent from the server as already known for this content source
* @return map of the same size as the existingPackages collection; empty map if existingPackages is <code>
* null</code>
*/
private Map<PackageDetailsKey, ContentProviderPackageDetails> unpack(
Collection<ContentProviderPackageDetails> existingPackages) {
Map<PackageDetailsKey, ContentProviderPackageDetails> map = new HashMap<PackageDetailsKey, ContentProviderPackageDetails>();
if (existingPackages == null) {
return map;
}
for (ContentProviderPackageDetails pkg : existingPackages) {
map.put(pkg.getKey(), pkg);
}
return map;
}
/**
* Parses out the cumulative patch version from the package title.
*
* @param title name of the package
* @return patch string if the title matches the expected title for a cumulative patch; <code>null</code> otherwise
*/
private String parseCumulativePatchVersion(String title) {
if (title.startsWith("JBoss AS ")) {
return title.substring(9);
} else if (title.startsWith("JBoss EAP ")) {
return title.substring(10);
} else if (title.startsWith("JBoss SOA ")) {
return title.substring(10);
}
return null;
}
/**
* Determines what plugin to use based on the version of jboss the feed we're parsing is for.
*
* @param jbossVersion the jboss version specified in the patch
* @return name of the plugin to handle this patch
*/
private String getPluginName(String jbossVersion) {
//this is very crude and hackish, but serves the purpose for now.
if (jbossVersion.trim().startsWith("5")) {
return JBOSS_AS5_PLUGIN_NAME;
} else {
return JBOSS_AS4_PLUGIN_NAME;
}
}
}