All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.cayenne.project.FileProjectSaver Maven / Gradle / Ivy

There is a newer version: 5.0-M1
Show newest version
/*****************************************************************
 *   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