/* * (c) Copyright 2010-2011 AgileBirds * * This file is part of OpenFlexo. * * OpenFlexo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * OpenFlexo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>. * */ package org.openflexo.foundation.rm; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Enumeration; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import org.openflexo.foundation.FlexoException; import org.openflexo.foundation.utils.FlexoProjectFile; import org.openflexo.toolbox.FileUtils; /** * This class represents a File Flexo resource. A File FlexoResource represent an object handled by Flexo Application Suite (all concerned * modules), which could be stored in a File, generally located in related {@link FlexoProject} project directory. * * @author sguerin */ public abstract class FlexoFileResource<RD extends FlexoResourceData> extends FlexoResource<RD> { /** * This constant traduces the delay accepted for the File System to effectively write a file on disk after the date it was requested. If * file is written after this delay, the ResourceManager will interprete it as a concurrent file modification requiring to be handled * properly. In fact, this is not a big problem but resource management may be affected. */ public static final long ACCEPTABLE_FS_DELAY = 4000; private static final Logger logger = Logger.getLogger(FlexoFileResource.class.getPackage().getName()); protected FlexoProjectFile resourceFile; // This variable is only used to be reset when we have written on disk. private Date _diskLastModifiedDate; private boolean _isSaving = false; /** * Constructor used for XML Serialization: never try to instanciate resource from this constructor * * @param builder */ public FlexoFileResource(FlexoProjectBuilder builder) { this(builder.project); builder.notifyResourceLoading(this); } public FlexoFileResource(FlexoProject aProject) { super(aProject); } /** * Override this */ @Override public boolean isToBeSerialized() { return true; } public FlexoProjectFile getResourceFile() { if (resourceFile != null) { resourceFile.setProject(project); } return resourceFile; } public void setResourceFile(FlexoProjectFile aFile) throws InvalidFileNameException { if (aFile == null) { throw new InvalidFileNameException("null file"); } if (!aFile.nameIsValid()) { if (logger.isLoggable(Level.SEVERE)) { logger.severe("File name: " + aFile.getRelativePath() + " is not valid for object of class " + getClass().getName()); } throw new InvalidFileNameException(aFile.getRelativePath()); } if (getProject().getFlexoRMResource() != null && !getProject().getFlexoRMResource().getIsLoading() && !(this instanceof FlexoRMResource)) { FlexoResource res = getProject().resourceForFileName(aFile); if (res != null && res != this) { if (logger.isLoggable(Level.SEVERE)) { logger.severe("The name " + ((FlexoFileResource) res).getResourceFile().getStringRepresentation() + " is equivalent to " + aFile.getStringRepresentation() + " and can therefore not be used twice!!"); logger.severe("The existing resource is " + ((FlexoFileResource) res).getFullyQualifiedName() + "\n The resource we try to associate to the same file is " + getFullyQualifiedName()); } throw new InvalidFileNameException(aFile.getRelativePath()); } } resourceFile = aFile; if (getProject() != null && resourceFile != null) { resourceFile.setProject(getProject()); } if (resourceFile != null && getProject() != null && resourceFile.getFile() != null) { File f = resourceFile.getFile(); for (File file : new ArrayList<File>(getProject().getFilesToDelete())) { if (file.getAbsolutePath().toLowerCase().equals(f.getAbsolutePath().toLowerCase())) { if (logger.isLoggable(Level.INFO)) { logger.info("Petit filou"); } getProject().removeFromFilesToDelete(file); if (!file.getAbsolutePath().equals(f.getAbsolutePath())) { if (logger.isLoggable(Level.INFO)) { logger.info("Super filou!"); } resourceFile.setFile(file); } break; } } } } public void recoverFile() { if (getFile() == null) { return; } if (getFile().exists()) { return; } if (getFile().getParentFile().exists()) { File[] files = getFile().getParentFile().listFiles(); for (int i = 0; i < files.length; i++) { File file = files[i]; if (file.getName().equalsIgnoreCase(getFile().getName())) { try { if (logger.isLoggable(Level.WARNING)) { logger.warning("Found file " + file.getAbsolutePath() + ". Using it and repairing project as well!"); } setResourceFile(new FlexoProjectFile(file, getProject())); break; } catch (InvalidFileNameException e) { e.printStackTrace(); } } } } } public File getFile() { if (resourceFile != null) { resourceFile.setProject(project); return resourceFile.getFile(); } else { return null; } } public boolean isConnected() { return getFile() != null && (resourceFile.getExternalRepository() == null || resourceFile.getExternalRepository().isConnected()); } public String getFileName() { return getFile() != null ? getFile().getName() : null; } public boolean renameFileTo(String name) throws InvalidFileNameException, IOException { File newFile = new File(getFile().getParentFile(), name); if (getFile().exists()) { FileUtils.rename(getFile(), newFile); if (getFile().exists()) { getFile().delete(); } resetDiskLastModifiedDate(); } if (getResourceFile().getExternalRepository() != null) { String relPath = getResourceFile().getRelativePath(); relPath = relPath.replace('\\', '/'); if (relPath.indexOf('/') > -1) { relPath = relPath.substring(0, relPath.lastIndexOf('/') + 1) + name; } else { relPath = name; } setResourceFile(new FlexoProjectFile(getProject(), getResourceFile().getExternalRepository(), relPath)); } else { setResourceFile(new FlexoProjectFile(newFile, getProject())); } return true; } /** * Returns the last modified date of the underlying file that Flexo has computed (or remembered) so that we get milliseconds precision * * @return the last modified date known by Flexo with milliseconds precision. */ public final synchronized Date getDiskLastModifiedDate() { if ((_diskLastModifiedDate == null || _diskLastModifiedDate.getTime() == 0 || isConnected() && !getFile().exists()) && !_isSaving) { if (getFile() != null && getFile().exists()) { _diskLastModifiedDate = FileUtils.getDiskLastModifiedDate(getFile()); } else { // logger.warning("File "+getFile().getAbsolutePath()+" doesn't exist"); _diskLastModifiedDate = new Date(0); // means never _lastWrittenOnDisk = new Date(0); } if (_lastWrittenOnDisk == null) { _lastWrittenOnDisk = _diskLastModifiedDate; } SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM HH:mm:ss SSS"); if (_diskLastModifiedDate.getTime() > _lastWrittenOnDisk.getTime() + ACCEPTABLE_FS_DELAY) { if (_lastWrittenOnDisk.getTime() != 0) { // Here we have written on disk, and somehow the disk last modified date is still bigger than the acceptable delay // This can happen sometimes if it takes too long to write on disk if (logger.isLoggable(Level.INFO)) { logger.info("Resource " + this + " : declared lastWrittenOnDisk date is anterior to current effective last modified date: which means that file on disk in newer than expected" + "_diskLastModifiedDate[" + simpleDateFormat.format(_diskLastModifiedDate) + "]" + " > lastWrittenOnDisk[" + simpleDateFormat.format(new Date(_lastWrittenOnDisk.getTime() + ACCEPTABLE_FS_DELAY)) + "]"); } } // Since we are in this block (diskLastModified was null see the top 'if'), we consider that it is some kind of bug in the // FS // and we update accordingly so that the resource checking thread won't think that the resource was updated by another // application _lastWrittenOnDisk = _diskLastModifiedDate; } else if (_lastWrittenOnDisk.getTime() - _diskLastModifiedDate.getTime() > ACCEPTABLE_FS_DELAY) { if (getFile().exists()) { // Warn it only if file exists: // otherwise it's normal logger.warning("Resource " + this + " : declared lastWrittenOnDisk date is posterior to current effective last modified date (with a delay, due to FS date implementation): which means that something strange happened" + "_diskLastModifiedDate[" + simpleDateFormat.format(_diskLastModifiedDate) + "]" + " < lastWrittenOnDisk[" + simpleDateFormat.format(_lastWrittenOnDisk) + "]"); // We should rather go back in time and consider that the information we stored is no longer correct. _lastWrittenOnDisk = _diskLastModifiedDate; } } } return _lastWrittenOnDisk; } // This is the date known by Flexo (with milliseconds precision) at which we have written on the disk. private Date _lastWrittenOnDisk; public final synchronized void _setLastWrittenOnDisk(Date aDate) { if (logger.isLoggable(Level.FINE) && aDate != null) { logger.fine("Resource " + this + "/" + hashCode() + " declared to be saved on disk on " + new SimpleDateFormat("dd/MM HH:mm:ss SSS").format(aDate)); } _diskLastModifiedDate = null; _lastWrittenOnDisk = aDate; } public final synchronized Date _getLastWrittenOnDisk() { return getDiskLastModifiedDate(); } public synchronized boolean hasMoreRecentThanExpectedDiskUpdate() { // if (logger.isLoggable(Level.FINEST)) // logger.finest("Resource " + this + " has more recent than expected disk update ?"); File f = getFile(); if (f == null) { if (resourceFile == null) { if (logger.isLoggable(Level.SEVERE)) { logger.severe("Found resource " + this + " without file)"); } } return false; } else if (!f.exists()) { return false; } if (!_isSaving) { Date fsDate = FileUtils.getDiskLastModifiedDate(f); Date knownDate = getDiskLastModifiedDate(); if (fsDate.getTime() > knownDate.getTime() + ACCEPTABLE_FS_DELAY) { // _setLastWrittenOnDisk(FileUtils.getDiskLastModifiedDate(getFile())); _setLastWrittenOnDisk(fsDate); // if (logger.isLoggable(Level.FINE)) // logger.fine("ID=" + getProject().getID() + " Resource " + this + "/" + hashCode() // + " has more recent than expected disk update " // + (new SimpleDateFormat("dd/MM HH:mm:ss SSS")).format(knownDate) + " < " // + (new SimpleDateFormat("dd/MM HH:mm:ss SSS")).format(fsDate)); return true; } } return false; } /** * This method should be used parsimoniously since RM will not detect disk updates. It should only be used in few cases, eg when * converting resources so that RM don't complain about updates in files, or when managing disk update accepting * */ protected synchronized void resetDiskLastModifiedDate() { if (getFile() == null || !getFile().exists()) { if (getFile() != null) { logger.warning("resetDiskLastModifiedDate() called for non existant file: " + getFile().getAbsolutePath()); } else { logger.warning("resetDiskLastModifiedDate() called for null file on resource " + this); } _setLastWrittenOnDisk(null); } else { _setLastWrittenOnDisk(new Date()); if (logger.isLoggable(Level.FINE)) { logger.fine("ID=" + getProject().getID() + " Resource " + this + " resetDiskLastModifiedDate() " + new SimpleDateFormat("dd/MM HH:mm:ss SSS").format(_lastWrittenOnDisk)); } } } protected FileWritingLock willWriteOnDisk() { if (logger.isLoggable(Level.FINE)) { logger.fine("willWriteOnDisk()"); } _isSaving = true; // This locking scheme was an attempt which seems to be unnecessary // Disactivated it. But kept for future needing if required // return new FileWritingLock(); return null; } protected void hasWrittenOnDisk(FileWritingLock lock) { if (logger.isLoggable(Level.FINE)) { logger.fine("hasWrittenOnDisk()"); } if (lock != null) { lock.start(); } else { notifyHasBeenWrittenOnDisk(); } } protected void notifyHasBeenWrittenOnDisk() { resetDiskLastModifiedDate(); _isSaving = false; } public class FileWritingLock extends Thread { @SuppressWarnings("hiding") private final Logger logger = Logger.getLogger(FlexoFileResource.FileWritingLock.class.getPackage().getName()); private Date _previousLastModified; private FileWritingLock() { super("FileWritingLock:" + getFile().getAbsolutePath()); if (getFile().exists()) { _previousLastModified = FileUtils.getDiskLastModifiedDate(getFile()); if (new Date().getTime() - _previousLastModified.getTime() < 1000) { // Last modified is this second: no way to know that file has been written, // Sets to null _previousLastModified = null; } } else { _previousLastModified = null; } } @Override public void run() { Date startChecking = new Date(); logger.info("Checking that file " + getFile().getAbsolutePath() + " has been successfully written"); boolean fileHasBeenWritten = false; while (new Date().getTime() <= startChecking.getTime() + ACCEPTABLE_FS_DELAY && !fileHasBeenWritten) { if (_previousLastModified == null) { fileHasBeenWritten = getFile().exists(); } else { Date currentLastModifiedDate = FileUtils.getDiskLastModifiedDate(getFile()); fileHasBeenWritten = currentLastModifiedDate.after(_previousLastModified); } if (!fileHasBeenWritten) { logger.info("Waiting file " + getFile().getAbsolutePath() + " to be written, thread " + this); try { Thread.sleep(200); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } if (!fileHasBeenWritten) { logger.warning("TIME-OUT occured while waiting file " + getFile().getAbsolutePath() + " to be written, thread " + this); } else { logger.info("File " + getFile().getAbsolutePath() + " has been written, thread " + this); } notifyHasBeenWrittenOnDisk(); } } protected boolean isSaving() { return _isSaving; } /** * Delete this resource by deleting the file */ @Override public synchronized void delete() { delete(true); } /** * Delete this resource. Delete file is flag deleteFile is true. */ public synchronized void delete(boolean deleteFile) { super.delete(); if (getFile() != null && getFile().exists() && deleteFile) { getProject().addToFilesToDelete(getFile()); if (logger.isLoggable(Level.INFO)) { logger.info("Will delete file " + getFile().getAbsolutePath() + " upon next save of RM"); } } } public synchronized boolean hasWritePermission() { return getFile() == null || (!getFile().exists() || getFile().canWrite()) && getFile().getParentFile() != null && (!getFile().getParentFile().exists() || getFile().getParentFile().canWrite()); } /** * Returns boolean indicating if merge scheme is implemented for this kind of resource If set to true, method #performMerge() must be * overriden in related class * * @return */ public boolean implementsResourceMerge() { return false; } /** * Perform merge between data read from a updated resource on disk and data stored in memory NOTE: must be overriden in subclasses if * relevant */ public synchronized void performMerge() throws FlexoException { if (logger.isLoggable(Level.WARNING)) { logger.warning("Merge NOT implemented for resource type " + getResourceType().getName()); } } /** * Perform update of specified resource data from data read from a updated resource on disk */ public synchronized void performDiskUpdate() throws FlexoException { forwardPropagateUpdate(); } protected synchronized void forwardPropagateUpdate() throws FlexoException { Vector resourcesThatAreConcerned = getResourceUpdateNotificationTargets(); if (logger.isLoggable(Level.FINE)) { logger.fine("Concerned resources: " + resourcesThatAreConcerned); } for (Enumeration en = resourcesThatAreConcerned.elements(); en.hasMoreElements();) { FlexoResource next = (FlexoResource) en.nextElement(); try { next.update(); } catch (ResourceDependencyLoopException e) { if (logger.isLoggable(Level.SEVERE)) { logger.log(Level.SEVERE, "Loop in dependant resources of " + this + "!", e); } } catch (FileNotFoundException e) { if (logger.isLoggable(Level.SEVERE)) { logger.log(Level.SEVERE, "File not found for " + next + "!", e); } } } } private Vector<FlexoResource> getResourceUpdateNotificationTargets() { Vector<FlexoResource> returned = new Vector<FlexoResource>(); Vector<FlexoResource> requesters = new Vector<FlexoResource>(); addResourceUpdateNotificationTargets(returned, requesters); return returned; } private void addResourceUpdateNotificationTargets(Vector<FlexoResource> targetList, Vector<FlexoResource> requesters) { if (requesters.contains(this)) { return; } requesters.add(this); Enumeration en = getSynchronizedResources().elements(); while (en.hasMoreElements()) { FlexoFileResource res = (FlexoFileResource) en.nextElement(); if (!targetList.contains(res) && propagateToSynchronizedResourceForResourceUpdateNotification(res)) { targetList.add(res); } // No recursion with synchronized resources // res.addResourceUpdateNotificationTargets(targetList,requesters); } en = getAlteredResources().elements(); while (en.hasMoreElements()) { FlexoFileResource res = (FlexoFileResource) en.nextElement(); if (!targetList.contains(res) && propagateToAlteredResourceForResourceUpdateNotification(res)) { targetList.add(res); } res.addResourceUpdateNotificationTargets(targetList, requesters); } } /** * Override this if required. Default behaviour is to return false, and thus, never propagate a ResourceUpdateNotification through * synchronized resources * * @param originResource * @param targetResource * @return */ protected boolean propagateToSynchronizedResourceForResourceUpdateNotification(FlexoResource targetResource) { return false; } /** * Override this if required. Default behaviour is to propagate ResourceUpdateNotification to all loaded storage resources * * @param originResource * @param targetResource * @return */ protected boolean propagateToAlteredResourceForResourceUpdateNotification(FlexoResource targetResource) { return targetResource instanceof FlexoStorageResource && ((FlexoStorageResource) targetResource).isLoaded(); } }