/*
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.extension.parser;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
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.xml.sax.SAXException;
import com.servoy.extension.ExtensionUtils;
import com.servoy.extension.ExtensionUtils.EntryInputStreamRunner;
import com.servoy.extension.IExtensionProvider;
import com.servoy.extension.IMessageProvider;
import com.servoy.extension.Message;
import com.servoy.extension.MessageKeeper;
import com.servoy.extension.VersionStringUtils;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.Pair;
import com.servoy.j2db.util.Utils;
/**
* This class parses an .exp file and provides it's contents in a friendly way.
* @author acostescu
*/
public class EXPParser implements IMessageProvider
{
public static final String EXTENSION_XML = "package.xml"; //$NON-NLS-1$
public static final String EXTENSION_SCHEMA = "servoy-extension.xsd"; //$NON-NLS-1$
// xml tag/attribute names & values
public static final String EXTENSION_ID = "extension-id"; //$NON-NLS-1$
public static final String EXTENSION_NAME = "extension-name"; //$NON-NLS-1$
public static final String VERSION = "version"; //$NON-NLS-1$
public static final String DEPENDENCIES = "dependencies"; //$NON-NLS-1$
public static final String SERVOY_DEPENDENCY = "servoy"; //$NON-NLS-1$
public static final String PATH = "path"; //$NON-NLS-1$
public static final String MAX_VERSION = "max-version"; //$NON-NLS-1$
public static final String MIN_VERSION = "min-version"; //$NON-NLS-1$
public static final String INCLUSIVE_MIN_MAX_ATTR = "inclusive"; //$NON-NLS-1$
public static final String FALSE_VALUE = "false"; //$NON-NLS-1$
public static final String EXTENSION_DEPENDENCY = "extension"; //$NON-NLS-1$
public static final String LIB_DEPENDENCY = "lib"; //$NON-NLS-1$
public static final String ID = "id"; //$NON-NLS-1$
public static final String CONTENT = "content"; //$NON-NLS-1$
public static final String IMPORT_SOLUTION = "importSolution"; //$NON-NLS-1$
public static final String IMPORT_STYLE = "importStyle"; //$NON-NLS-1$
public static final String TEAM_PROJECT_SET = "teamProjectSet"; //$NON-NLS-1$
public static final String ECLIPSE_UPDATE_SITE = "eclipseUpdateSite"; //$NON-NLS-1$
public static final String URL = "url"; //$NON-NLS-1$
public static final String INFO = "info"; //$NON-NLS-1$
public static final String ICON = "icon"; //$NON-NLS-1$
public static final String DESCRIPTION = "description"; //$NON-NLS-1$
public static final String RESTART = "requiresRestart"; //$NON-NLS-1$
protected File expFile;
protected FullDependencyMetadata dependencyMetadata;
protected ExtensionConfiguration xml;
protected boolean dependencyParsed = false;
protected boolean allParsed = false;
protected MessageKeeper messages = new MessageKeeper();
public EXPParser(File expFile)
{
this.expFile = expFile;
}
@SuppressWarnings("nls")
public FullDependencyMetadata parseDependencyInfo()
{
if (!dependencyParsed)
{
dependencyParsed = true;
ZipFile zipFile = null;
try
{
zipFile = new ZipFile(expFile);
ZipEntry extensionFile = zipFile.getEntry(EXTENSION_XML);
if (extensionFile != null)
{
Boolean adheresToSchema = runOnEntry(zipFile, extensionFile, new ValidateAgainstSchema(expFile.getName()));
if (Boolean.TRUE.equals(adheresToSchema))
{
dependencyMetadata = runOnEntry(zipFile, extensionFile, new ParseDependencyMetadata(expFile.getName(), messages));
}
}
else
{
messages.addError("Reading extension package '" + expFile.getName() + "' failed; it will be ignored. Reason: missing 'package.xml'.");
}
}
catch (ZipException e)
{
messages.addError("Reading extension package '" + expFile.getName() + "' failed; it will be ignored. Reason: " + e.getMessage() + ".");
Debug.trace("Reading extension package '" + expFile.getName() + "' failed; it will be ignored.", e);
}
catch (IOException e)
{
messages.addError("Reading extension package '" + expFile.getName() + "' failed; it will be ignored. Reason: " + e.getMessage() + ".");
Debug.trace("Reading extension package '" + expFile.getName() + "' failed; it will be ignored.", e);
}
finally
{
if (zipFile != null)
{
try
{
zipFile.close();
}
catch (IOException e)
{
// ignore
}
}
}
}
return dependencyMetadata;
}
@SuppressWarnings("nls")
public ExtensionConfiguration parseWholeXML()
{
if (!allParsed)
{
allParsed = true;
parseDependencyInfo(); // will parse it if not already parsed; also checks against schema
if (dependencyMetadata != null)
{
// valid dependency metadata and was validated against schema; parse the rest of the XML
try
{
Pair<Boolean, ExtensionConfiguration> result = ExtensionUtils.runOnEntry(expFile, EXTENSION_XML, new ParseAllRemaining(expFile,
dependencyMetadata));
xml = result.getRight();
if (Boolean.FALSE.equals(result.getLeft()))
{
// this shouldn't happen as package.xml was found before when parsing the same zip file for dependency info
Debug.warn("'package.xml' no longer found when trying to parse it for extension '" + dependencyMetadata.id + "'. File: '" +
expFile.getCanonicalPath() + "'.");
}
}
catch (ZipException e)
{
messages.addError("Zip problems encountered while trying to parse entire 'package.xml' for extension '" + dependencyMetadata.id +
"'. Reason: " + e.getMessage() + ".");
Debug.trace(e);
}
catch (IOException e)
{
messages.addError("IO problems encountered while trying to parse entire 'package.xml' for extension '" + dependencyMetadata.id +
"'. Reason: " + e.getMessage() + ".");
Debug.trace(e);
}
}
}
return xml;
}
protected <T> T runOnEntry(ZipFile zipFile, ZipEntry extensionFile, EntryInputStreamRunner<T> runner) throws IOException
{
InputStream is = null;
BufferedInputStream bis = null;
try
{
is = zipFile.getInputStream(extensionFile);
bis = new BufferedInputStream(is);
return runner.runOnEntryInputStream(bis);
}
finally
{
Utils.closeInputStream(bis);
}
}
public Message[] getMessages()
{
return messages.getMessages();
}
public void clearMessages()
{
// messages.clear(); as all is cached, clearing error messages is misleading, cause they won't reappear; cached data is returned
}
// this is done separately, although schema is used when parsing also, because in
// that case we only receive some hard-to-differentiate error and parsing continues,
// but we actually want to parse only if XML is valid from the schema's point of view
@SuppressWarnings("nls")
protected class ValidateAgainstSchema implements EntryInputStreamRunner<Boolean>
{
private final String zipFileName;
public ValidateAgainstSchema(String zipFileName)
{
this.zipFileName = zipFileName;
}
public Boolean runOnEntryInputStream(InputStream is)
{
// verify that XML adheres to our schema
boolean adheresToSchema = false;
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
if (factory != null)
{
try
{
Schema schema = factory.newSchema(IExtensionProvider.class.getResource(EXTENSION_SCHEMA));
Validator validator = schema.newValidator();
Source source = new StreamSource(is);
try
{
validator.validate(source);
adheresToSchema = true;
}
catch (SAXException ex)
{
messages.addError("Invalid 'package.xml' in package '" + zipFileName + "'; .xsd validation failed. Reason: " + ex.getMessage() + ".");
Debug.trace("Invalid 'package.xml' in package '" + zipFileName + "'; .xsd validation failed.", ex);
}
catch (IOException ex)
{
messages.addError("Invalid 'package.xml' in package '" + zipFileName + "'; .xsd validation failed. Reason: " + ex.getMessage() + ".");
Debug.trace("Invalid 'package.xml' in package '" + zipFileName + "'; .xsd validation failed.", ex);
}
}
catch (SAXException ex)
{
messages.addError("Unable to validate 'package.xml' against the .xsd. Please report this problem to Servoy.");
Debug.error("Error compiling 'servoy-extension.xsd'.");
}
}
else
{
messages.addError("Unable to validate 'package.xml' against the .xsd. Please report this problem to Servoy.");
Debug.error("Cannot find schema factory.");
}
return Boolean.valueOf(adheresToSchema);
}
}
/**
* Parses the whole XML file (except dependency that is already parsed) to construct an in-memory representation of it.
*/
protected class ParseAllRemaining implements EntryInputStreamRunner<ExtensionConfiguration>
{
private final File zipFile;
private final FullDependencyMetadata dependencyInfo;
public ParseAllRemaining(File zipFile, FullDependencyMetadata dependencyInfo)
{
this.zipFile = zipFile;
this.dependencyInfo = dependencyInfo;
}
public ExtensionConfiguration runOnEntryInputStream(InputStream is)
{
// TODO parse the rest of the xml
ExtensionConfiguration wholeXML = null;
SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema"); //$NON-NLS-1$
if (factory != null)
{
Schema schema = null;
try
{
// prepare to verify that XML adheres to our schema; because of this schema defined default values will be set as well when parsing
schema = factory.newSchema(IExtensionProvider.class.getResource(EXTENSION_SCHEMA));
}
catch (SAXException ex)
{
messages.addError("Unable to validate 'package.xml' against the .xsd. Please report this problem to Servoy."); //$NON-NLS-1$
Debug.error("Error compiling 'servoy-extension.xsd'."); //$NON-NLS-1$
}
if (schema != null)
{
try
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setSchema(schema);
DocumentBuilder db = dbf.newDocumentBuilder();
db.setErrorHandler(new ParseDependencyMetadataErrorHandler(zipFile.getName(), messages));
Document doc = db.parse(is); // should we use UTF-8 here?
Element root = doc.getDocumentElement(); // "servoy-extension" tag
root.normalize();
// as this was already validated by schema, we need less null-checks and less structure checks
Content content = null;
NodeList list = root.getElementsByTagName(CONTENT);
if (list != null && list.getLength() == 1)
{
List<String> solutionToImportPaths = new ArrayList<String>();
List<String> styleToImportPaths = new ArrayList<String>();
List<String> teamProjectSetPaths = new ArrayList<String>();
List<String> eclipseUpdateSiteURLs = new ArrayList<String>();
Element contentNode = (Element)list.item(0);
Element element;
int i = 0;
list = contentNode.getElementsByTagName(IMPORT_SOLUTION);
while (list != null && list.getLength() > i)
{
element = ((Element)list.item(i++));
solutionToImportPaths.add(element.getAttribute(PATH));
}
i = 0;
list = contentNode.getElementsByTagName(IMPORT_STYLE);
while (list != null && list.getLength() > i)
{
element = ((Element)list.item(i++));
styleToImportPaths.add(element.getAttribute(PATH));
}
i = 0;
list = contentNode.getElementsByTagName(TEAM_PROJECT_SET);
while (list != null && list.getLength() > i)
{
element = ((Element)list.item(i++));
teamProjectSetPaths.add(element.getAttribute(PATH));
}
i = 0;
list = contentNode.getElementsByTagName(ECLIPSE_UPDATE_SITE);
while (list != null && list.getLength() > i)
{
element = ((Element)list.item(i++));
eclipseUpdateSiteURLs.add(element.getAttribute(URL));
}
content = new Content(solutionToImportPaths.size() > 0 ? solutionToImportPaths.toArray(new String[solutionToImportPaths.size()])
: null, styleToImportPaths.size() > 0 ? styleToImportPaths.toArray(new String[styleToImportPaths.size()]) : null,
teamProjectSetPaths.size() > 0 ? teamProjectSetPaths.toArray(new String[teamProjectSetPaths.size()]) : null,
eclipseUpdateSiteURLs.size() > 0 ? eclipseUpdateSiteURLs.toArray(new String[eclipseUpdateSiteURLs.size()]) : null);
}
Info info = null;
list = root.getElementsByTagName(INFO);
if (list != null && list.getLength() == 1)
{
Element infoNode = (Element)list.item(0);
String description = null;
list = infoNode.getElementsByTagName(DESCRIPTION);
if (list != null && list.getLength() == 1)
{
description = list.item(0).getTextContent();
}
String iconPath = null;
list = infoNode.getElementsByTagName(ICON);
if (list != null && list.getLength() == 1)
{
iconPath = ((Element)list.item(0)).getAttribute(PATH);
}
String url = null;
list = infoNode.getElementsByTagName(URL);
if (list != null && list.getLength() == 1)
{
url = list.item(0).getTextContent();
}
info = new Info(iconPath, url, description);
}
boolean requiresRestart = false;
list = root.getElementsByTagName(RESTART);
if (list != null && list.getLength() == 1)
{
requiresRestart = true;
}
wholeXML = new ExtensionConfiguration(dependencyInfo, content, info, requiresRestart);
}
catch (ParserConfigurationException e)
{
messages.addError("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'. Reason: " + e.getMessage() + "."); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
Debug.trace("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'.", e); //$NON-NLS-1$ //$NON-NLS-2$
}
catch (SAXException e)
{
messages.addError("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'. Reason: " + e.getMessage() + "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
Debug.trace("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'.", e); //$NON-NLS-1$ //$NON-NLS-2$
}
catch (IOException e)
{
messages.addError("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'. Reason: " + e.getMessage() + "."); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
Debug.trace("Cannot parse 'package.xml' in package '" + zipFile.getName() + "'.", e); //$NON-NLS-1$//$NON-NLS-2$
}
catch (FactoryConfigurationError e)
{
messages.addError("Unable to parse 'package.xml'. Please report this problem to Servoy."); //$NON-NLS-1$
Debug.error("Cannot find document builder factory."); //$NON-NLS-1$
}
}
}
else
{
messages.addError("Unable to validate 'package.xml' against the .xsd. Please report this problem to Servoy."); //$NON-NLS-1$
Debug.error("Cannot find schema factory."); //$NON-NLS-1$
}
return wholeXML;
}
// gets & creates a (possibly exclusive or unbounded) min or max version string from the element
protected String getMinMaxVersion(Element element, String minOrMax)
{
String minMaxVersion = VersionStringUtils.UNBOUNDED;
NodeList verNode = element.getElementsByTagName(minOrMax);
if (verNode != null && verNode.getLength() == 1)
{
minMaxVersion = verNode.item(0).getTextContent();
NamedNodeMap attrs = verNode.item(0).getAttributes();
if (attrs != null)
{
Node attr = attrs.getNamedItem(INCLUSIVE_MIN_MAX_ATTR);
if (attr != null && FALSE_VALUE.equals(attr.getNodeValue()))
{
minMaxVersion = VersionStringUtils.createExclusiveVersionString(minMaxVersion);
}
}
}
return minMaxVersion;
}
}
}