/******************************************************************************* * Copyright (c) 1998, 2016 Oracle and/or its affiliates. All rights reserved. * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 * which accompanies this distribution. * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html * and the Eclipse Distribution License is available at * http://www.eclipse.org/org/documents/edl-v10.php. * * Contributors: * Oracle - initial API and implementation from Oracle TopLink ******************************************************************************/ package org.eclipse.persistence.tools.workbench.mappingsio; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.eclipse.persistence.oxm.XMLMarshaller; import org.eclipse.persistence.tools.workbench.mappingsmodel.MWModel; import org.eclipse.persistence.tools.workbench.mappingsmodel.MWNominative; import org.eclipse.persistence.tools.workbench.mappingsmodel.ProjectSubFileComponentContainer; import org.eclipse.persistence.tools.workbench.mappingsmodel.project.MWProject; import org.eclipse.persistence.tools.workbench.utility.CollectionTools; import org.eclipse.persistence.tools.workbench.utility.io.FileTools; import org.eclipse.persistence.tools.workbench.utility.string.StringTools; /** * A new instance of this class is created for each project write: * new ProjectWriter(ioManager, project, listener).write() */ class ProjectWriter { /** The I/O manager that created this writer. */ private ProjectIOManager ioManager; /** The project to be written. */ private MWProject project; /** A listener that will be notified whenever an "expected" file is missing. */ private FileNotFoundListener listener; /** The collection of sub-component writers. */ private SubComponentWriter[] subComponentWriters; /** A collection of the changes (deletes and writes) that need to be written. */ private Collection changes; // ********** constructors ********** ProjectWriter(ProjectIOManager ioManager, MWProject project, FileNotFoundListener listener) { super(); this.ioManager = ioManager; this.project = project; if (project.getSaveDirectory() == null) { throw new NullPointerException("The project's save directory must be set before it is written."); } this.listener = listener; } // ********** public stuff ********** /** * Write out the project and its sub-components to its save directory. */ void write() throws ReadOnlyFilesException { // build up the sub-component writers this.subComponentWriters = this.buildSubComponentWriters(); // gather up all the changes once this.changes = this.buildProposedChanges(); this.checkForReadOnlyFiles(); this.commit(); this.resetProject(); } @Override public String toString() { return StringTools.buildToStringFor(this, this.project.getName()); } // ********** internal stuff ********** /** * Build writers for all the project's sub-components that are * written out separately: classes, tables, descriptors. */ private SubComponentWriter[] buildSubComponentWriters() { return new SubComponentWriter[] { new SubComponentWriter(this.project.getClassRepository()), new SubComponentWriter(this.project.getMetaDataSubComponentContainer()), new SubComponentWriter(this.project.getDescriptorRepository()), }; } /** * Build all the changes during initialization. * They will be checked for read-only and * committed during the write. */ private Collection buildProposedChanges() { Collection proposedChanges = new ArrayList(); if (this.project.hasChangedMainProjectSaveFile()) { proposedChanges.add(new Write(this.project, this.project.saveFile())); } for (int i = 0; i < this.subComponentWriters.length; i++) { this.subComponentWriters[i].addChangesTo(proposedChanges); } return proposedChanges; } /** * If any of the writes or deletes correspond to a read-only * file, throw an exception. Clients can get a list of the read-only * files from the exception. */ private void checkForReadOnlyFiles() throws ReadOnlyFilesException { Collection readOnlyFiles = new ArrayList(this.changes.size()); for (Iterator stream = this.changes.iterator(); stream.hasNext(); ) { Change change = (Change) stream.next(); change.addReadOnlyFilesTo(readOnlyFiles); } if ( ! readOnlyFiles.isEmpty()) { throw new ReadOnlyFilesException(readOnlyFiles); } } /** * Commit all the changes. */ private void commit() { for (Iterator stream = this.changes.iterator(); stream.hasNext(); ) { ((Change) stream.next()).commit(); } } /** * Update the project, now that it has been written. */ private void resetProject() { // clear all the dirty flags in the project this.project.markEntireBranchClean(); // save the sub-component names for the next write... for (int i = 0; i < this.subComponentWriters.length; i++) { this.subComponentWriters[i].resetContainer(); } } /** * Return the base directory for all the project files. * The project file is in this directory, while all the other * files are in subdirectories of this directory. */ File baseDirectory() { return this.project.getSaveDirectory(); } XMLMarshaller marshaller() { return this.ioManager.getMarshaller(); } String defaultFileNameExtension() { return this.ioManager.defaultFileNameExtension(); } String subDirectoryNameFor(Object container) { return this.ioManager.subDirectoryNameFor(container); } void fireFileNotFound(File missingFile) { this.ioManager.fireFileNotFound(this.listener, missingFile); } // ********** inner classes ********** /** * Delegate sub-component-related behavior to this class. */ private class SubComponentWriter { /** the container that holds the sub-components */ private ProjectSubFileComponentContainer container; SubComponentWriter(ProjectSubFileComponentContainer container) { this.container = container; } /** * Determine which sub-components need to be written and * which need to be deleted; add them all to the list of changes. */ void addChangesTo(Collection proposedChanges) { String ext = ProjectWriter.this.defaultFileNameExtension(); // build the sub-directory that holds the sub-components String subDirectoryName = ProjectWriter.this.subDirectoryNameFor(this.container); File subDirectory = new File(ProjectWriter.this.baseDirectory(), subDirectoryName); // build the writes... Set currentNames = new HashSet(); // ...while simultaneously gathering up the current names Set<File> currentFiles = new HashSet<>(); // and files for (Iterator stream = this.container.projectSubFileComponents(); stream.hasNext(); ) { MWModel subComponent = (MWModel) stream.next(); String subComponentName = ((MWNominative) subComponent).getName(); currentNames.add(subComponentName); String subComponentFileName = FileTools.FILE_NAME_ENCODER.encode(subComponentName); File subComponentFile = new File(subDirectory, subComponentFileName + ext); currentFiles.add(subComponentFile); if (subComponent.isDirtyBranch()) { proposedChanges.add(new Write(subComponent, subComponentFile)); } } // build the deletes Collection originalNames = CollectionTools.set(this.container.originalProjectSubFileComponentNames()); Collection deletedNames = this.calculateDeleted(originalNames, currentNames); for (Iterator stream = deletedNames.iterator(); stream.hasNext(); ) { String deleteFileName = FileTools.FILE_NAME_ENCODER.encode((String) stream.next()); File deleteFile = new File(subDirectory, deleteFileName + ext); boolean found = false; for (File cur : currentFiles) { if (cur.getName().equalsIgnoreCase(deleteFile.getName())) { found = true; break; } } if (found) { // do nothing // 'currentFiles' will only "contain" the 'deleteFile' on Windows, where two files can be // "equal" when their names differ only by case; this will happen if the user renames // an object by only changing the case of the object's name: on Linux, the appropriate // file will be added (e.g. "foo.xml") and the appropriate file removed (e.g. "FOO.xml"); // on Windows though, the appropriate file will be added (e.g. "foo.xml"), but then it // will be deleted immediately afterwards (when we try to delete "FOO.xml") by the // Delete Change if we don't perform this check } else { proposedChanges.add(new Delete(deleteFile)); } } } /** * Return the collection of objects that were removed from * the specified starting collection as a result of its transformation * into the specified ending collection. */ private Collection calculateDeleted(Collection start, Collection end) { Collection deleted = new HashSet(start); deleted.removeAll(end); return deleted; } /** * Store the current names in the container, * for use on the next write. */ void resetContainer() { Set currentNames = new HashSet(); for (Iterator stream = this.container.projectSubFileComponents(); stream.hasNext(); ) { currentNames.add(((MWNominative) stream.next()).getName()); } this.container.setOriginalProjectSubFileComponentNames(currentNames); } /** * Return the collection of objects that were added to * the specified starting collection as a result of its transformation * into the specified ending collection. */ // unused... // private Collection calculateAdded(Collection start, Collection end) { // Collection added = new HashSet(end); // added.removeAll(start); // return added; // } } /** * Record the file to be changed (deleted or written). */ private abstract class Change { protected File file; Change(File file) { super(); this.file = file; } /** * If the change is associated with a read-only file, * add the file to the specified collection. */ void addReadOnlyFilesTo(Collection readOnlyFiles) { if (this.file.exists() && ! this.file.canWrite()) { readOnlyFiles.add(this.file); } } /** * Commit the change to disk. */ abstract void commit(); @Override public String toString() { return StringTools.buildToStringFor(this, this.file); } XMLMarshaller marshaller() { return ProjectWriter.this.marshaller(); } } /** * Record the file to be deleted. */ private class Delete extends Change { Delete(File file) { super(file); } /** * Simply delete the file. */ @Override void commit() { if (this.file.exists()) { this.file.delete(); } else { ProjectWriter.this.fireFileNotFound(this.file); } } } /** * Pair an object with the file to which it will be written. */ private class Write extends Change { private Object object; Write(Object object, File file) { super(file); this.object = object; } /** * Use TopLink to write the object to the file. */ @Override void commit() { try { this.commit2(); } catch (IOException ex) { throw new RuntimeException(ex); } } private void commit2() throws IOException { this.checkDirectory(); if (this.file.exists()) { // we do this because Windows file names are case-insensitive; // delete the original file so we don't re-use it for an object with the // same name but different case (e.g. we don't want to store the // "foo" object in the "FOO.xml" file, which is what would happen // on Windows if we don't delete the original file); this also allows // the project to be moved to a Linux machine without incident this.file.delete(); } OutputStream stream = null; try { stream = new BufferedOutputStream(new FileOutputStream(this.file), 2048); this.marshaller().marshal(this.object, stream); } finally { if (stream != null) { stream.close(); } } } private void checkDirectory() { File dir = this.file.getParentFile(); if ( ! dir.exists()) { if ( ! dir.mkdirs()) { throw new RuntimeException("unable to create directory: " + dir.getAbsolutePath()); } } } } }