org.apache.cayenne.project.FileProjectSaver Maven / Gradle / Ivy
/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.project;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.configuration.ConfigurationNameMapper;
import org.apache.cayenne.configuration.ConfigurationNode;
import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
import org.apache.cayenne.di.Inject;
import org.apache.cayenne.project.extension.ProjectExtension;
import org.apache.cayenne.project.extension.SaverDelegate;
import org.apache.cayenne.resource.Resource;
import org.apache.cayenne.resource.URLResource;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.util.XMLEncoder;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* A ProjectSaver saving project configuration to the file system.
*
* @since 3.1
*/
public class FileProjectSaver implements ProjectSaver {
@Inject
protected ConfigurationNameMapper nameMapper;
protected ConfigurationNodeVisitor resourceGetter;
protected ConfigurationNodeVisitor> saveableNodesGetter;
protected String fileEncoding;
protected Collection extensions;
protected SaverDelegate delegate;
public FileProjectSaver(@Inject List extensions) {
resourceGetter = new ConfigurationSourceGetter();
saveableNodesGetter = new SaveableNodesGetter();
// this is not configurable yet... probably doesn't have to be
fileEncoding = "UTF-8";
this.extensions = extensions;
Collection delegates = new ArrayList<>(extensions.size());
for(ProjectExtension extension : extensions) {
delegates.add(extension.createSaverDelegate());
}
delegate = new CompoundSaverDelegate(delegates);
}
@Override
public String getSupportedVersion() {
return String.valueOf(Project.VERSION);
}
@Override
public void save(Project project) {
save(project, project.getConfigurationResource(), true);
}
@Override
public void saveAs(Project project, Resource baseDirectory) {
if (baseDirectory == null) {
throw new NullPointerException("Null 'baseDirectory'");
}
save(project, baseDirectory, false);
}
void save(Project project, Resource baseResource, boolean deleteOldResources) {
Collection nodes = project.getRootNode().acceptVisitor(saveableNodesGetter);
Collection units = new ArrayList<>(nodes.size());
delegate.setBaseDirectory(baseResource);
for(ConfigurationNode node : nodes) {
String targetLocation = nameMapper.configurationLocation(node);
Resource targetResource = baseResource.getRelativeResource(targetLocation);
units.add(createSaveUnit(node, targetResource, null));
for(ProjectExtension extension : extensions) {
ConfigurationNodeVisitor namingDelegate = extension.createNamingDelegate();
SaverDelegate unitSaverDelegate = extension.createSaverDelegate();
String fileName = node.acceptVisitor(namingDelegate);
if(fileName != null) {
// not null means that this should go to a separate file
targetResource = baseResource.getRelativeResource(fileName);
units.add(createSaveUnit(node, targetResource, unitSaverDelegate));
}
}
}
checkAccess(units);
try {
saveToTempFiles(units);
saveCommit(units);
} finally {
clearTempFiles(units);
}
try {
if (deleteOldResources) {
clearRenamedFiles(units);
Collection unusedResources = project.getUnusedResources();
for (SaveUnit unit : units) {
unusedResources.remove(unit.sourceConfiguration.getURL());
}
deleteUnusedFiles(unusedResources);
}
} catch (IOException ex) {
throw new CayenneRuntimeException(ex);
}
// I guess we should reset projects state regardless of the value of
// 'deleteOldResources'
project.getUnusedResources().clear();
}
SaveUnit createSaveUnit(ConfigurationNode node, Resource targetResource, SaverDelegate delegate) {
SaveUnit unit = new SaveUnit();
unit.node = node;
unit.delegate = delegate;
unit.sourceConfiguration = node.acceptVisitor(resourceGetter);
if (unit.sourceConfiguration == null) {
unit.sourceConfiguration = targetResource;
}
// attempt to convert targetResource to a File... if that fails,
// FileProjectSaver is not appropriate for handling a given project..
URL targetUrl = targetResource.getURL();
try {
unit.targetFile = Util.toFile(targetUrl);
} catch (IllegalArgumentException e) {
throw new CayenneRuntimeException("Can't save configuration to the following location: '%s'. "
+ "Is this a valid file location?. (%s)", e, targetUrl, e.getMessage());
}
return unit;
}
void checkAccess(Collection units) {
for (SaveUnit unit : units) {
File targetFile = unit.targetFile;
File parent = targetFile.getParentFile();
if (!parent.exists()) {
if (!parent.mkdirs()) {
throw new CayenneRuntimeException("Error creating directory tree for '%s'",
parent.getAbsolutePath());
}
}
if (targetFile.isDirectory()) {
throw new CayenneRuntimeException("Target file '%s' is a directory", targetFile.getAbsolutePath());
}
if (targetFile.exists() && !targetFile.canWrite()) {
throw new CayenneRuntimeException("Can't write to file '%s'", targetFile.getAbsolutePath());
}
}
}
void saveToTempFiles(Collection units) {
for (SaveUnit unit : units) {
String name = unit.targetFile.getName();
if (name.length() < 3) {
name = "cayenne-project";
}
File parent = unit.targetFile.getParentFile();
try {
unit.targetTempFile = File.createTempFile(name, null, parent);
} catch (IOException e) {
throw new CayenneRuntimeException("Error creating temp file (%s)", e, e.getMessage());
}
if (unit.targetTempFile.exists()) {
unit.targetTempFile.delete();
}
try (PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(
new FileOutputStream(unit.targetTempFile), fileEncoding))) {
saveToTempFile(unit, printWriter);
} catch (UnsupportedEncodingException e) {
throw new CayenneRuntimeException("Unsupported encoding '%s' (%s)", e, fileEncoding, e.getMessage());
} catch (FileNotFoundException e) {
throw new CayenneRuntimeException("File not found '%s' (%s)", e, unit.targetTempFile.getAbsolutePath(),
e.getMessage());
}
}
}
void saveToTempFile(SaveUnit unit, PrintWriter printWriter) {
ConfigurationNodeVisitor> visitor;
if(unit.delegate == null) {
visitor = new ConfigurationSaver(printWriter, getSupportedVersion(), delegate);
} else {
XMLEncoder encoder = new XMLEncoder(printWriter, "\t", getSupportedVersion());
encoder.println("");
unit.delegate.setXMLEncoder(encoder);
visitor = unit.delegate;
}
unit.node.acceptVisitor(visitor);
}
void saveCommit(Collection units) {
for (SaveUnit unit : units) {
File targetFile = unit.targetFile;
// Per CAY-2119, this is an ugly hack to force Windows to unlock the file that was previously locked by
// our process. Without it, the delete operation downstream would fail
System.gc();
if (targetFile.exists()) {
if (!targetFile.delete()) {
throw new CayenneRuntimeException("Unable to remove old master file '%s'",
targetFile.getAbsolutePath());
}
}
File tempFile = unit.targetTempFile;
if (!tempFile.renameTo(targetFile)) {
throw new CayenneRuntimeException("Unable to move '%s' to '%s'", tempFile.getAbsolutePath(),
targetFile.getAbsolutePath());
}
unit.targetTempFile = null;
try {
if(unit.delegate == null) {
unit.node.acceptVisitor(new ConfigurationSourceSetter(new URLResource(targetFile.toURI().toURL())));
}
} catch (MalformedURLException e) {
throw new CayenneRuntimeException("Malformed URL for file '%s'", e, targetFile.getAbsolutePath());
}
}
}
private void clearTempFiles(Collection units) {
for (SaveUnit unit : units) {
if (unit.targetTempFile != null && unit.targetTempFile.exists()) {
unit.targetTempFile.delete();
unit.targetTempFile = null;
}
}
}
private void clearRenamedFiles(Collection units) throws IOException {
for (SaveUnit unit : units) {
if (unit.sourceConfiguration == null) {
continue;
}
URL sourceUrl = unit.sourceConfiguration.getURL();
File sourceFile;
try {
sourceFile = Util.toFile(sourceUrl);
} catch (IllegalArgumentException e) {
// ignore non-file configurations...
continue;
}
if (!sourceFile.exists()) {
continue;
}
// compare against ALL unit target files, not just the current
// unit... if the
// target matches, skip this file
boolean isTarget = false;
for (SaveUnit xunit : units) {
if (isFilesEquals(sourceFile, xunit.targetFile)) {
isTarget = true;
break;
}
}
if (!isTarget) {
if (!sourceFile.delete()) {
throw new CayenneRuntimeException("Could not delete file '%s'", sourceFile.getCanonicalPath());
}
}
}
}
private boolean isFilesEquals(File firstFile, File secondFile) throws IOException {
boolean isFirstFileExists = firstFile.exists();
boolean isSecondFileExists = secondFile.exists();
String firstFilePath = firstFile.getCanonicalPath();
String secondFilePath = secondFile.getCanonicalPath();
return isFirstFileExists && isSecondFileExists && firstFilePath.equals(secondFilePath);
}
private void deleteUnusedFiles(Collection unusedResources) throws IOException {
for (URL unusedResource : unusedResources) {
File unusedFile;
try {
unusedFile = Util.toFile(unusedResource);
} catch (IllegalArgumentException e) {
// ignore non-file configurations...
continue;
}
if (!unusedFile.exists()) {
continue;
}
if (!unusedFile.delete()) {
throw new CayenneRuntimeException("Could not delete file '%s'", unusedFile.getCanonicalPath());
}
}
}
class SaveUnit {
private ConfigurationNode node;
private SaverDelegate delegate;
// source can be an abstract resource, but target is always a file...
private Resource sourceConfiguration;
private File targetFile;
private File targetTempFile;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy