/******************************************************************************* | |
* 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()); | |
} | |
} | |
} | |
} | |
} |