/***************************************************************** * 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 * * http://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.resource.Resource; import org.apache.cayenne.resource.URLResource; import org.apache.cayenne.util.Util; 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; /** * A ProjectSaver saving project configuration to the file system. * * @since 3.1 */ public class FileProjectSaver implements ProjectSaver { @Inject protected ConfigurationNameMapper nameMapper; protected ConfigurationNodeVisitor<Resource> resourceGetter; protected ConfigurationNodeVisitor<Collection<ConfigurationNode>> saveableNodesGetter; protected String fileEncoding; public FileProjectSaver() { resourceGetter = new ConfigurationSourceGetter(); saveableNodesGetter = new SaveableNodesGetter(); // this is not configurable yet... probably doesn't have to be fileEncoding = "UTF-8"; } public String getSupportedVersion() { return "9"; } public void save(Project project) { save(project, project.getConfigurationResource(), true); } 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<ConfigurationNode> nodes = project.getRootNode().acceptVisitor(saveableNodesGetter); Collection<SaveUnit> units = new ArrayList<>(nodes.size()); for (ConfigurationNode node : nodes) { units.add(createSaveUnit(node, baseResource)); } checkAccess(units); try { saveToTempFiles(units); saveCommit(units); } finally { clearTempFiles(units); } try { if (deleteOldResources) { clearRenamedFiles(units); Collection<URL> 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 baseResource) { SaveUnit unit = new SaveUnit(); unit.node = node; unit.sourceConfiguration = node.acceptVisitor(resourceGetter); String targetLocation = nameMapper.configurationLocation(node); Resource targetResource = baseResource.getRelativeResource(targetLocation); 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<SaveUnit> 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<SaveUnit> units) { for (SaveUnit unit : units) { String name = unit.targetFile.getName(); if (name == null || 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) { unit.node.acceptVisitor(new ConfigurationSaver(printWriter, getSupportedVersion())); } void saveCommit(Collection<SaveUnit> 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 { 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<SaveUnit> units) { for (SaveUnit unit : units) { if (unit.targetTempFile != null && unit.targetTempFile.exists()) { unit.targetTempFile.delete(); unit.targetTempFile = null; } } } private void clearRenamedFiles(Collection<SaveUnit> 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<URL> 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; // source can be an abstract resource, but target is always a file... private Resource sourceConfiguration; private File targetFile; private File targetTempFile; } }