blob: 4a9f7a9e225fd27c4cfd3203eb6cecafac8124b6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 1998, 2013 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.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;
import org.eclipse.persistence.oxm.XMLMarshaller;
/**
* 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();
}
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 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);
if (currentFiles.contains(deleteFile)) {
// 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();
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.
*/
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.
*/
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());
}
}
}
}
}