/******************************************************************************* * Copyright (c) 2016 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.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.persistence.PersistenceException; import org.apache.commons.io.output.FileWriterWithEncoding; import org.apache.commons.lang.Validate; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.jubula.client.archive.dto.ExportInfoDTO; import org.eclipse.jubula.client.archive.dto.ProjectDTO; import org.eclipse.jubula.client.archive.dto.TestresultSummaryDTO; import org.eclipse.jubula.client.archive.i18n.Messages; import org.eclipse.jubula.client.core.Activator; import org.eclipse.jubula.client.core.businessprocess.IParamNameMapper; import org.eclipse.jubula.client.core.businessprocess.IWritableComponentNameCache; import org.eclipse.jubula.client.core.businessprocess.ParamNameBPDecorator; import org.eclipse.jubula.client.core.businessprocess.ProjectNameBP; 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.persistence.ProjectPM; 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.eclipse.osgi.util.NLS; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; /** @author BREDEX GmbH */ public class JsonStorage { /** Extension of project file */ public static final String PJT = "pjt"; //$NON-NLS-1$ /** Extension of test result summaries file */ public static final String RST = "rst"; //$NON-NLS-1$ /** Extension of info file */ public static final String NFO = "nfo"; //$NON-NLS-1$ /** */ private static final String TMP_EXCHANGE_FOLDER_NAME = "JubArchiveTemp"; //$NON-NLS-1$ /** Standard logging */ private static Logger log = LoggerFactory.getLogger(JsonStorage.class); /** * Save a project as JUB to a file or return the serialized project as * an ProjectDTO, if fileName == null! * * @param proj original project object * @param fileName Jubula file name * @param includeTestResultSummaries true if project contain test result summaries * @param monitor loader monitor * @param console * @return ProjectDTO * @throws PMException * @throws ProjectDeletedException * @throws InterruptedException */ public static ProjectDTO save(IProjectPO proj, String fileName, boolean includeTestResultSummaries, IProgressMonitor monitor, IProgressConsole console) throws PMException, ProjectDeletedException { monitor.beginTask(Messages.GatheringProjectData, getWorkToSave(proj, includeTestResultSummaries)); monitor.subTask(Messages.ImportJsonStoragePreparing); Validate.notNull(proj); try { if (fileName == null) { JsonExporter exporter = new JsonExporter(proj, monitor); return exporter.getProjectDTO(); } writeToFile(proj, monitor, fileName, includeTestResultSummaries); } catch (FileNotFoundException e) { log.info(Messages.File + StringConstants.SPACE + Messages.NotFound); console.writeStatus(new Status(IStatus.WARNING, Activator.PLUGIN_ID, Messages.NotFound)); 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.warn(Messages.GeneralIoExeption); console.writeStatus(new Status(IStatus.WARNING, Activator.PLUGIN_ID, Messages.GeneralIoExeption)); throw new PMSaveException(Messages.GeneralIoExeption + e.toString(), MessageIDs.E_FILE_IO); } } catch (PersistenceException e) { log.warn(Messages.CouldNotInitializeProxy + StringConstants.DOT); console.writeStatus(new Status(IStatus.WARNING, Activator.PLUGIN_ID, Messages.CouldNotInitializeProxy)); throw new PMSaveException(e.getMessage(), MessageIDs.E_DATABASE_GENERAL); } catch (OperationCanceledException e) { // Operation was cancelled. log.info(Messages.ExportOperationCanceled); console.writeStatus(new Status(IStatus.WARNING, Activator.PLUGIN_ID, Messages.ExportOperationCanceled)); } return null; } /** Save a project as JUB witch contains an info file about exportation, a project file * and a result file * * @param proj original project object * @param monitor loader monitor * @param fileName Jubula file name * @param includeTestResultSummaries true if project contain test result summaries * @throws ProjectDeletedException * @throws PMException * @throws IOException * @throws InterruptedException */ private static void writeToFile(IProjectPO proj, IProgressMonitor monitor, String fileName, boolean includeTestResultSummaries) throws ProjectDeletedException, PMException, IOException { String dir = Files.createTempDirectory(TMP_EXCHANGE_FOLDER_NAME) .toString() + File.separatorChar; String infoFileName = dir + NFO; String projectFileName = dir + PJT; String testResultFileName = dir + RST; ArrayList<String> fileList = new ArrayList<String>(); ObjectMapper mapper = new ObjectMapper(); // changed when upgrading Jackson to 2.6.2 to 2.5 // previously it was NON_EMPTY, but that resulted in 0 Integers // being not serialised with 2.6.2, so they behaved the same way as null // Integers. Non-serialised fields are initialised by the // default () constructor of DTOs. // NON-EMPTY: empty and null Strings, empty and null Collections and null Integers / Doubles are not serialised // NON-NULL: null Objects are not serialised mapper.setSerializationInclusion(Include.NON_NULL); ExportInfoDTO exportDTO = new ExportInfoDTO(); exportDTO.setQualifier( ImportExportUtil.DATE_FORMATTER.format( new Date())); exportDTO.setEncoding(StandardCharsets.UTF_8.name()); exportDTO.setVersion(JsonVersion.CURRENTLY_JSON_VERSION); try ( FileWriterWithEncoding infoWriter = new FileWriterWithEncoding( infoFileName, StandardCharsets.UTF_8); FileWriterWithEncoding projectWriter = new FileWriterWithEncoding( projectFileName, StandardCharsets.UTF_8); FileWriterWithEncoding resultWriter = new FileWriterWithEncoding( testResultFileName, StandardCharsets.UTF_8)) { mapper.writeValue(infoWriter, exportDTO); fileList.add(infoFileName); JsonExporter exporter = new JsonExporter(proj, monitor); ProjectDTO projectDTO = exporter.getProjectDTO(); mapper.writeValue(projectWriter, projectDTO); fileList.add(projectFileName); if (includeTestResultSummaries) { exporter.writeTestResultSummariesToFile(resultWriter); fileList.add(testResultFileName); } monitor.subTask(Messages.ImportJsonStorageCompress); zipIt(fileName, fileList); } catch (Exception e) { fileList.add(fileName); throw e; } finally { fileList.add(dir); deleteFiles(fileList); } } /** * @param files what we need to delete * @throws IOException */ private static void deleteFiles(List<String> files) { for (String fileSrt : files) { try { Files.deleteIfExists(new File(fileSrt).toPath()); } catch (IOException e) { log.warn(Messages.CantDeleteFile + fileSrt); } } } /** * @param project The project for which the work is predicted. * @param includeTestResultSummaries true if test result summary needed. * @return The predicted amount of work required to save a project. */ public static int getWorkToSave(IProjectPO project, boolean includeTestResultSummaries) { return JsonExporter.getPredictedWork(project, includeTestResultSummaries); } /** * @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, false); } return totalWork; } /** * @param url of import file * @param paramNameMapper * @param compNameCache * @param assignNewGuid <code>true</code> if the project and all subnodes * should be assigned new GUIDs. Otherwise * <code>false</code>. * @param assignNewVersion if <code>true</code> the project will have * a new project version number, otherwise it will * have the stored project version from the dto. * @param monitor * @param io console * @return IProjectPO new project object * @throws JBVersionException * @throws PMReadException * @throws InterruptedException * @throws ToolkitPluginException * @throws PMSaveException */ public IProjectPO readProject(URL url, ParamNameBPDecorator paramNameMapper, final IWritableComponentNameCache compNameCache, boolean assignNewGuid, boolean assignNewVersion, IProgressMonitor monitor, IProgressConsole io) throws JBVersionException, PMReadException, InterruptedException, ToolkitPluginException { SubMonitor subMonitor = SubMonitor.convert(monitor, Messages .ImportFileBPReading, 2); IProjectPO projectPO = null; monitor.subTask(Messages.ImportJsonStoragePreparing); try (InputStream urlInputStream = url.openStream(); ZipInputStream zipInputStream = new ZipInputStream( urlInputStream, StandardCharsets.UTF_8);) { ObjectMapper mapper = new ObjectMapper(); mapper.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); Map<String, Class> fileTypeMapping = new HashMap<>(); fileTypeMapping.put(NFO, ExportInfoDTO.class); fileTypeMapping.put(PJT, ProjectDTO.class); TypeReference tr = new TypeReference<ArrayList< TestresultSummaryDTO>>() { /* anonymous inner type */ }; Map<String, Object> allDTOs = new HashMap<>(); for (int i = 0; i < 3; i++) { ZipEntry entry = zipInputStream.getNextEntry(); String entryName = entry.getName(); Class entryTypeMapping = fileTypeMapping.get(entryName); allDTOs.put(entryName, entryTypeMapping != null ? mapper.readValue(zipInputStream, entryTypeMapping) : mapper.readValue(zipInputStream, tr)); } ExportInfoDTO exportDTO = (ExportInfoDTO) allDTOs.get(NFO); checkMinimumRequiredJSONVersion(exportDTO); ProjectDTO projectDTO = (ProjectDTO) allDTOs.get(PJT); if (!assignNewGuid && projectExists(projectDTO)) { existProjectHandling(io, projectDTO); return null; } projectPO = load(projectDTO, subMonitor.newChild(1), io, assignNewGuid, assignNewVersion, paramNameMapper, compNameCache, false, exportDTO); JsonImporter importer = new JsonImporter(monitor, io, false, exportDTO); List<TestresultSummaryDTO> summaryDTOs = (List<TestresultSummaryDTO>) allDTOs.get(RST); importer.initTestResultSummaries(subMonitor.newChild(1), summaryDTOs, projectPO); } catch (IOException e) { log.warn("error during import", e); //$NON-NLS-1$ // If the operation has been canceled, then this is just // a result of canceling the IO. if (!monitor.isCanceled()) { log.info(Messages.GeneralIoExeption); throw new PMReadException(Messages.InvalidImportFile, MessageIDs.E_IO_EXCEPTION); } } return projectPO; } /** * @param dto storage of the project * @param monitor * @param io console * @param assignNewGuid <code>true</code> if the project and all subnodes * should be assigned new GUIDs. Otherwise * <code>false</code>. * @param assignNewVersion if <code>true</code> the project will have * a new project version number, otherwise it will * have the stored project version from the dto. * @param paramNameMapper * @param compNameCache * @param skipTrackingInformation * @param exportInfo the exported verison information * @return IProjectPO * @throws JBVersionException * @throws InterruptedException * @throws PMReadException * @throws ToolkitPluginException */ public static IProjectPO load(ProjectDTO dto, IProgressMonitor monitor, IProgressConsole io, boolean assignNewGuid, boolean assignNewVersion, IParamNameMapper paramNameMapper, IWritableComponentNameCache compNameCache, boolean skipTrackingInformation, ExportInfoDTO exportInfo) throws JBVersionException, InterruptedException, PMReadException, ToolkitPluginException { IProjectPO projectPO = null; try { JsonImporter importer = new JsonImporter(monitor, io, skipTrackingInformation, exportInfo); projectPO = importer.createProject(dto, assignNewGuid, assignNewVersion, paramNameMapper, compNameCache); } catch (InvalidDataException e) { throw new PMReadException(Messages.InvalidImportFile, e.getErrorId()); } return projectPO; } /** * @param io console * @param projectDTO */ private void existProjectHandling(IProgressConsole io, ProjectDTO projectDTO) { String msg = NLS.bind(Messages.ErrorMessageIMPORT_PROJECT_FAILED, new String [] {ProjectNameBP.getInstance().getName( projectDTO.getUuid(), false)}) + StringConstants.NEWLINE + NLS.bind(Messages.ErrorMessageIMPORT_PROJECT_FAILED_EXISTING, new String [] {projectDTO.getName(), projectDTO.getProjectVersion().toString()}); io.writeStatus(new Status(IStatus.WARNING, Activator.PLUGIN_ID, msg)); } /** * @param dto the project dto * @throws JBVersionException * in case of version conflict between given dto and minimum dto * version number; if these versions do not fit the current * available converter are not able to convert the given project * dto properly. */ private void checkMinimumRequiredJSONVersion(ExportInfoDTO dto) throws JBVersionException { if (dto.getVersion() == null || !JsonVersion.isCompatible(dto.getVersion())) { List<String> errorMsgs = new ArrayList<String>(); errorMsgs.add(Messages.JubImporterProjectJUBTooOld); throw new JBVersionException( Messages.JubImporterProjectJUBTooOld, MessageIDs.E_LOAD_PROJECT_XML_VERSION_ERROR, errorMsgs); } } /** * Zip it * @param zipFile output ZIP file location * @param fileList it contains files what we would like to compress * @throws IOException */ private static void zipIt(String zipFile, ArrayList<String> fileList) throws IOException { byte[] buffer = new byte[1024]; FileOutputStream fos = new FileOutputStream(zipFile); ZipOutputStream zos = new ZipOutputStream(fos); for (String file : fileList) { String fileName = file.substring(file.lastIndexOf( File.separator) + 1); ZipEntry ze = new ZipEntry(fileName); zos.putNextEntry(ze); FileInputStream in = new FileInputStream(file); int len; while ((len = in.read(buffer)) > 0) { zos.write(buffer, 0, len); } in.close(); } zos.closeEntry(); zos.close(); } /** * @param dto ProjectDTO what we wanted to import. * @return <code>true</code> if another project with the same GUID and * version number as the currently imported project already * exists in the database. Otherwise <code>false</code>. */ private boolean projectExists(ProjectDTO dto) { return ProjectPM.doesProjectVersionExist(dto.getUuid(), dto.getMajorProjectVersion(), dto.getMinorProjectVersion(), dto.getMicroProjectVersion(), dto.getProjectVersionQualifier()); } }