| /******************************************************************************* |
| * Copyright (c) 2004, 2010 BREDEX GmbH. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BREDEX GmbH - initial API and implementation and/or initial documentation |
| *******************************************************************************/ |
| package org.eclipse.jubula.client.archive; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.net.URL; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Timer; |
| import java.util.TimerTask; |
| |
| import javax.persistence.PersistenceException; |
| |
| import org.apache.commons.io.IOUtils; |
| import org.apache.commons.io.output.FileWriterWithEncoding; |
| import org.apache.commons.lang.Validate; |
| import org.apache.xmlbeans.XmlException; |
| import org.apache.xmlbeans.XmlOptions; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.NullProgressMonitor; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.jubula.client.archive.i18n.Messages; |
| import org.eclipse.jubula.client.archive.schema.ContentDocument; |
| import org.eclipse.jubula.client.archive.schema.ContentDocument.Content; |
| import org.eclipse.jubula.client.archive.schema.Project; |
| import org.eclipse.jubula.client.core.businessprocess.IParamNameMapper; |
| import org.eclipse.jubula.client.core.businessprocess.IWritableComponentNameCache; |
| import org.eclipse.jubula.client.core.model.IProjectPO; |
| import org.eclipse.jubula.client.core.persistence.PMException; |
| import org.eclipse.jubula.client.core.persistence.PMReadException; |
| import org.eclipse.jubula.client.core.persistence.PMSaveException; |
| import org.eclipse.jubula.client.core.progress.IProgressConsole; |
| import org.eclipse.jubula.toolkit.common.exception.ToolkitPluginException; |
| import org.eclipse.jubula.tools.internal.constants.StringConstants; |
| import org.eclipse.jubula.tools.internal.exception.InvalidDataException; |
| import org.eclipse.jubula.tools.internal.exception.JBVersionException; |
| import org.eclipse.jubula.tools.internal.exception.ProjectDeletedException; |
| import org.eclipse.jubula.tools.internal.messagehandling.MessageIDs; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * @author BREDEX GmbH |
| * @created 11.01.2006 |
| */ |
| @SuppressWarnings("synthetic-access") |
| public class XmlStorage { |
| /** |
| * Helper for IO-related tasks that can be cancelled. |
| * |
| * @author BREDEX GmbH |
| * @created Dec 3, 2007 |
| */ |
| private static class IOCanceller extends TimerTask { |
| |
| /** The monitor for which the IO is taking place. */ |
| private IProgressMonitor m_monitor; |
| |
| /** The writer in which the IO is taking place. */ |
| private FileWriterWithEncoding m_writer; |
| |
| /** The Timer used to schedule regular interruption checks. */ |
| private Timer m_timer; |
| |
| /** |
| * Constructor |
| * |
| * @param monitor |
| * The monitor for which the IO is taking place. |
| * @param writer |
| * The writer in which the IO is taking place. |
| */ |
| public IOCanceller(IProgressMonitor monitor, |
| FileWriterWithEncoding writer) { |
| |
| m_monitor = monitor; |
| m_writer = writer; |
| m_timer = new Timer(); |
| } |
| |
| /** |
| * Signal that the IO task is about to start. |
| */ |
| public void startTask() { |
| m_timer.schedule(this, 1000, 1000); |
| } |
| |
| /** |
| * Signal that the IO task has finished. |
| */ |
| public void taskFinished() { |
| m_timer.cancel(); |
| } |
| |
| /** |
| * Check whether the operation has been cancelled. If so, the output |
| * stream will be closed. |
| */ |
| private void checkTask() { |
| if (m_monitor.isCanceled()) { |
| try { |
| m_writer.close(); |
| } catch (IOException e) { |
| log.error(Messages.ErrorWhileCloseOS, e); |
| } |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void run() { |
| checkTask(); |
| } |
| } |
| |
| /** XML header encoding definition */ |
| public static final String RECOMMENDED_CHAR_ENCODING = "UTF-8"; //$NON-NLS-1$ |
| |
| /** |
| * The supported character encodings. |
| */ |
| private static final String[] SUPPORTED_CHAR_ENCODINGS = |
| new String[]{RECOMMENDED_CHAR_ENCODING, "UTF-16"}; //$NON-NLS-1$ |
| |
| /** |
| * the current XML schema namespace |
| */ |
| private static final String SCHEMA_NAMESPACE = "http://www.eclipse.org/jubula/client/archive/schema"; //$NON-NLS-1$ |
| |
| /** name of GUIdancer import/export XML element representing Exec Test Cases */ |
| private static final String EXEC_TC_XML_ELEMENT_NAME = "usedTestcase"; //$NON-NLS-1$ |
| |
| /** XPATH statement for selecting all Exec Test Cases */ |
| private static final String XPATH_FOR_EXEC_TCS = "declare namespace s='" + SCHEMA_NAMESPACE + "' " + //$NON-NLS-1$//$NON-NLS-2$ |
| ".//s:" + EXEC_TC_XML_ELEMENT_NAME; //$NON-NLS-1$ |
| |
| /** standard logging */ |
| private static Logger log = LoggerFactory.getLogger(XmlStorage.class); |
| |
| /** |
| * the old xml schema namespace (< 5.0) |
| */ |
| private static final String OLD_SCHEMA_NAMESPACE = "http://www.bredexsw.com/guidancer/client/importer/gdschema"; //$NON-NLS-1$ |
| |
| /** |
| * Generate an XML document representing the content of the project. |
| * |
| * @param project |
| * the root of the data |
| * @param includeTestResultSummaries |
| * Whether to save the Test Result Summaries as well. |
| * @param monitor |
| * The progress monitor for this potentially long-running |
| * operation. |
| * @return an input stream to the XML representation, or |
| * <code>null</code> if the operation was cancelled. |
| * @throws PMException |
| * of io or encoding errors |
| * @throws ProjectDeletedException |
| * in case of current project is already deleted |
| */ |
| private static InputStream save(IProjectPO project, |
| boolean includeTestResultSummaries, IProgressMonitor monitor) |
| throws ProjectDeletedException, PMException { |
| XmlOptions genOpts = new XmlOptions(); |
| genOpts.setCharacterEncoding(RECOMMENDED_CHAR_ENCODING); |
| genOpts.setSaveInner(); |
| genOpts.setSaveAggressiveNamespaces(); |
| genOpts.setUseDefaultNamespace(); |
| // Don't make use of pretty print due to http://eclip.se/395788 |
| // genOpts.setSavePrettyPrint(); |
| |
| ContentDocument contentDoc = ContentDocument.Factory |
| .newInstance(genOpts); |
| Content content = contentDoc.addNewContent(); |
| |
| Project prj = content.addNewProject(); |
| |
| try { |
| new XmlExporter(monitor).fillProject( |
| prj, project, includeTestResultSummaries); |
| } catch (OperationCanceledException oce) { |
| // Operation was cancelled. |
| log.info(Messages.ExportOperationCanceled); |
| return null; |
| } |
| |
| if (monitor.isCanceled()) { |
| // Operation was cancelled. |
| return null; |
| } |
| |
| XmlOptions options = new XmlOptions(genOpts); |
| |
| Collection errors = new ArrayList(); |
| options.setErrorListener(errors); |
| if (!contentDoc.validate(options)) { |
| StringBuilder msgs = new StringBuilder(StringConstants.NEWLINE); |
| for (Object msg : errors) { |
| msgs.append(msg); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug(Messages.ValidateFailed |
| + StringConstants.COLON, msgs); |
| log.debug(Messages.ValidateFailed |
| + StringConstants.COLON, contentDoc); |
| } |
| throw new PMSaveException( |
| "XML" + Messages.ValidateFailed + msgs.toString(), //$NON-NLS-1$ |
| MessageIDs.E_FILE_IO); |
| } |
| return contentDoc.newInputStream(genOpts); |
| } |
| |
| /** |
| * Takes the supplied input stream and parses it. According to the content |
| * an instance of IProjetPO along with its associated components is created. |
| * |
| * @param projectXmlStream |
| * input stream for XML representation of a project |
| * @param majorVersion |
| * Major version number for the created object, or |
| * <code>null</code> if the version from the imported XML should |
| * be used. |
| * @param minorVersion |
| * Minor version number for the created object, or |
| * <code>null</code> if the version from the imported XML should |
| * be used. |
| * @param microVersion |
| * Micro version number for the created object, or |
| * <code>null</code> if the version from the imported XML should |
| * be used. |
| * @param versionQualifier |
| * Version Qualifier number for the created object, or |
| * <code>null</code> if the version from the imported XML should |
| * be used. |
| * @param paramNameMapper |
| * mapper to resolve param names |
| * @param compNameCache |
| * cache to resolve component names |
| * @param monitor |
| * The progress monitor for this potentially long-running |
| * operation. |
| * @param io |
| * the device to write the import output |
| * @param skipTrackingInformation |
| * whether to skip importing of tracked information |
| * @return an transient IProjectPO and its components |
| * @throws PMReadException |
| * in case of a invalid XML string |
| * @throws JBVersionException |
| * in case of version conflict between used toolkits of imported |
| * project and the installed toolkit plug-ins |
| * @throws InterruptedException |
| * if the operation was canceled. |
| * @throws ToolkitPluginException |
| * in case of the toolkit of the project is not supported |
| */ |
| public static IProjectPO load(InputStream projectXmlStream, |
| Integer majorVersion, Integer minorVersion, |
| Integer microVersion, String versionQualifier, |
| IParamNameMapper paramNameMapper, |
| IWritableComponentNameCache compNameCache, IProgressMonitor monitor, |
| IProgressConsole io, boolean skipTrackingInformation) |
| throws PMReadException, JBVersionException, |
| InterruptedException, ToolkitPluginException { |
| ContentDocument contentDoc; |
| try { |
| contentDoc = getContent(projectXmlStream); |
| Project projectXml = contentDoc.getContent().getProject(); |
| int numExecTestCases = |
| projectXml.selectPath(XPATH_FOR_EXEC_TCS).length; |
| |
| monitor.beginTask(StringConstants.EMPTY, numExecTestCases + 1); |
| monitor.worked(1); |
| |
| XmlImporter xmlImporter = new XmlImporter(monitor, io, |
| skipTrackingInformation); |
| if ((majorVersion != null || versionQualifier != null)) { |
| return xmlImporter.createProject( |
| projectXml, majorVersion, minorVersion, |
| microVersion, versionQualifier, paramNameMapper, |
| compNameCache); |
| } |
| return xmlImporter.createProject(projectXml, |
| paramNameMapper, compNameCache); |
| } catch (XmlException e) { |
| throw new PMReadException(Messages.InvalidImportFile, |
| MessageIDs.E_LOAD_PROJECT); |
| } catch (InvalidDataException e) { |
| throw new PMReadException(Messages.InvalidImportFile, |
| e.getErrorId()); |
| } |
| } |
| |
| /** |
| * Reads the content from a string containing XML data |
| * @param projectXmlStream an input stream to the project XML data |
| * @return a ContentDocument which represents the XML data |
| * @throws XmlException if the parsing fails |
| * @throws PMReadException if the validation fails |
| */ |
| private static ContentDocument getContent(InputStream projectXmlStream) |
| throws XmlException, PMReadException { |
| Map<String, String> substitutes = new HashMap<String, String>(); |
| substitutes.put(OLD_SCHEMA_NAMESPACE, SCHEMA_NAMESPACE); |
| XmlOptions options = new XmlOptions(); |
| options.setLoadSubstituteNamespaces(substitutes); |
| |
| ContentDocument contentDoc = null; |
| try { |
| contentDoc = ContentDocument.Factory.parse( |
| projectXmlStream, options); |
| Collection errors = new ArrayList(); |
| options.setErrorListener(errors); |
| if (!contentDoc.validate(options)) { |
| StringBuilder msgs = new StringBuilder(StringConstants.NEWLINE); |
| for (Object msg : errors) { |
| msgs.append(msg); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug(Messages.ValidateFailed |
| + StringConstants.COLON, msgs); |
| log.debug(Messages.ValidateFailed |
| + StringConstants.COLON, contentDoc); |
| } |
| throw new PMReadException(Messages.InvalidImportFile |
| + msgs.toString(), MessageIDs.E_LOAD_PROJECT); |
| } |
| } catch (IOException e) { |
| log.error(e.getLocalizedMessage(), e); |
| throw new PMReadException(e.getLocalizedMessage(), |
| MessageIDs.E_LOAD_PROJECT); |
| } finally { |
| IOUtils.closeQuietly(projectXmlStream); |
| } |
| return contentDoc; |
| } |
| |
| /** |
| * Save a project as XML to a file or return the serialized project as |
| * an input stream, if fileName == null! |
| * |
| * @param proj |
| * project to be saved |
| * @param fileName |
| * name for file to save or null, if wanting to get the project |
| * as serialized string |
| * @param includeTestResultSummaries |
| * Whether to save the Test Result Summaries as well. |
| * @param monitor |
| * The progress monitor for this potentially long-running |
| * operation. |
| * @param writeToSystemTempDir |
| * Indicates whether the project has to be written to the system |
| * temp directory |
| * @param listOfProjectFiles |
| * If a project is written into the temp dir then the written |
| * file is added to the list, if the list is not null. |
| * @return an input stream to the serialized project if fileName == null<br> |
| * or<br> |
| * <b>Returns:</b><br> |
| * null otherwise. Always returns <code>null</code> if the save |
| * operation was canceled. |
| * @throws PMException |
| * if save failed for any reason |
| * @throws ProjectDeletedException |
| * in case of current project is already deleted |
| */ |
| public static InputStream save(IProjectPO proj, String fileName, |
| boolean includeTestResultSummaries, |
| IProgressMonitor monitor, boolean writeToSystemTempDir, |
| List<File> listOfProjectFiles) |
| throws ProjectDeletedException, PMException { |
| |
| monitor.beginTask(Messages.GatheringProjectData, |
| getWorkToSave(proj)); |
| |
| Validate.notNull(proj); |
| FileWriterWithEncoding fWriter = null; |
| try { |
| InputStream projXMLStream = XmlStorage.save(proj, |
| includeTestResultSummaries, monitor); |
| |
| if (fileName == null) { |
| return projXMLStream; |
| } |
| |
| if (writeToSystemTempDir) { |
| File fileInTempDir = createTempFile(fileName); |
| if (listOfProjectFiles != null) { |
| listOfProjectFiles.add(fileInTempDir); |
| } |
| fWriter = new FileWriterWithEncoding(fileInTempDir, |
| RECOMMENDED_CHAR_ENCODING); |
| } else { |
| fWriter = new FileWriterWithEncoding(fileName, |
| RECOMMENDED_CHAR_ENCODING); |
| } |
| |
| IOCanceller canceller = new IOCanceller(monitor, fWriter); |
| canceller.startTask(); |
| IOUtils.copy(projXMLStream, fWriter, RECOMMENDED_CHAR_ENCODING); |
| canceller.taskFinished(); |
| } catch (FileNotFoundException e) { |
| log.debug(Messages.File + StringConstants.SPACE |
| + Messages.NotFound, e); |
| throw new PMSaveException(Messages.File + StringConstants.SPACE |
| + fileName + Messages.NotFound + StringConstants.COLON |
| + StringConstants.SPACE |
| + e.toString(), MessageIDs.E_FILE_IO); |
| } catch (IOException e) { |
| // If the operation has been canceled, then this is just |
| // a result of canceling the IO. |
| if (!monitor.isCanceled()) { |
| log.debug(Messages.GeneralIoExeption, e); |
| throw new PMSaveException(Messages.GeneralIoExeption |
| + e.toString(), MessageIDs.E_FILE_IO); |
| } |
| } catch (PersistenceException e) { |
| log.debug(Messages.CouldNotInitializeProxy |
| + StringConstants.DOT, e); |
| throw new PMSaveException(e.getMessage(), |
| MessageIDs.E_DATABASE_GENERAL); |
| } finally { |
| if (fWriter != null) { |
| try { |
| fWriter.close(); |
| } catch (IOException e) { |
| // just log, we are already done |
| log.error(Messages.CantCloseOOS + fWriter.toString(), e); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Creates a file with the given name in the system temp directory. |
| * @param fileName The name of the file to be created in temp dir |
| * @return the created file |
| */ |
| private static File createTempFile(String fileName) throws IOException { |
| final String fileNamePrefix; |
| final String fileNameSuffix; |
| int dotIndex = fileName.lastIndexOf(StringConstants.DOT); |
| |
| if (dotIndex < 0) { |
| fileNamePrefix = fileName; |
| fileNameSuffix = StringConstants.EMPTY; |
| } else { |
| fileNamePrefix = fileName.substring(0, dotIndex) |
| + StringConstants.UNDERSCORE; |
| fileNameSuffix = fileName.substring(dotIndex); |
| } |
| File fileInTempDir = |
| File.createTempFile(fileNamePrefix, fileNameSuffix); |
| |
| return fileInTempDir; |
| } |
| |
| /** |
| * Reads the content of the file and returns it as a string. |
| * |
| * @param fileURL |
| * The URL of the project to import |
| * @return an input stream to the URL content |
| * @throws PMReadException |
| * If the file couldn't be read (wrong file name, IOException) |
| */ |
| private static InputStream openStreamToProjectURL(URL fileURL) |
| throws PMReadException { |
| try { |
| checkCharacterEncoding(fileURL); |
| return fileURL.openStream(); |
| } catch (IOException e) { |
| log.debug(e.getLocalizedMessage(), e); |
| throw new PMReadException(e.toString(), MessageIDs.E_FILE_IO); |
| } |
| } |
| |
| /** |
| * Checks the character encoding of the given XML-URL. |
| * |
| * @param xmlProjectURL |
| * a URL-object which must point a valid XML-Structure. |
| * @see SUPPORTED_CHAR_ENCODINGS |
| * @return the encoding or throws exception if not supported encoding used |
| * @throws IOException |
| * in case of reading error. |
| */ |
| public static String checkCharacterEncoding(URL xmlProjectURL) |
| throws IOException { |
| for (String encoding : SUPPORTED_CHAR_ENCODINGS) { |
| try (InputStream xmlProjectStream = xmlProjectURL.openStream(); |
| BufferedReader reader = new BufferedReader( |
| new InputStreamReader(xmlProjectStream, encoding))) { |
| final String firstLine = reader.readLine(); |
| if (firstLine != null && firstLine.contains(encoding)) { |
| return encoding; |
| } |
| } |
| } |
| throw new IOException(Messages.NoSupportedFileEncoding |
| + StringConstants.EXCLAMATION_MARK); |
| } |
| |
| /** |
| * read a <code> GeneralStorage </code> object from filename <b> call |
| * getProjectAutToolKit(String filename) at first </b> |
| * |
| * @param fileURL |
| * URL of the project file to read |
| * @param paramNameMapper |
| * mapper to resolve param names |
| * @param compNameCache |
| * cache to resolve component names |
| * @param monitor |
| * The progress monitor for this potentially long-running |
| * operation. |
| * @param io |
| * the device to write the import output |
| * @return the persisted object |
| * @throws PMReadException |
| * in case of error |
| * @throws JBVersionException |
| * in case of version conflict between used toolkits of imported |
| * project and the installed Toolkit Plugins |
| * @throws InterruptedException |
| * if the operation was canceled. |
| * @throws ToolkitPluginException |
| * in case of the toolkit of the project is not supported |
| */ |
| public IProjectPO readProject(URL fileURL, |
| IParamNameMapper paramNameMapper, |
| IWritableComponentNameCache compNameCache, |
| IProgressMonitor monitor, IProgressConsole io) throws PMReadException, |
| JBVersionException, InterruptedException, ToolkitPluginException { |
| return load(openStreamToProjectURL(fileURL), null, |
| null, null, null, paramNameMapper, compNameCache, monitor, io, |
| false); |
| } |
| |
| /** |
| * |
| * @param project The project for which the work is predicted. |
| * @return The predicted amount of work required to save a project. |
| */ |
| public static int getWorkToSave(IProjectPO project) { |
| return new XmlExporter(new NullProgressMonitor()) |
| .getPredictedWork(project); |
| } |
| |
| /** |
| * |
| * @param projectsToSave The projects for which the work is predicted. |
| * @return The predicted amount of work required to save the |
| * given projects. |
| */ |
| public static int getWorkToSave(List<IProjectPO> projectsToSave) { |
| int totalWork = 0; |
| |
| for (IProjectPO project : projectsToSave) { |
| totalWork += getWorkToSave(project); |
| } |
| |
| return totalWork; |
| } |
| } |