/******************************************************************************* * Copyright (c) 2006, 2015 BEA Systems, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * wharley@bea.com - initial API and implementation *******************************************************************************/ package org.eclipse.jdt.apt.core.internal.generatedfile; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.apt.core.internal.AptPlugin; import org.eclipse.jdt.apt.core.internal.util.ManyToMany; /** * A bidirectional many-to-many map from parent files to generated files. * This extends the functionality of ManyToMany by adding serialization. * The object also tracks attributes of the generated files. */ public class GeneratedFileMap extends ManyToMany<IFile, IFile> { public enum Flags { /** Non-source files, e.g., text or xml. */ NONSOURCE; } // Version 2 since Eclipse 3.3.1: add ability to track attributes of generated files private static final int SERIALIZATION_VERSION = 2; private final IProject _proj; private final Map<IFile, Set<Flags>> _flags = new HashMap<>(); public GeneratedFileMap(IProject proj) { _proj = proj; readState(); } /* (non-Javadoc) * @see org.eclipse.jdt.apt.core.internal.util.ManyToMany#clear() */ @Override public synchronized boolean clear() { _flags.clear(); return super.clear(); } /* (non-Javadoc) * @see org.eclipse.jdt.apt.core.internal.util.ManyToMany#remove(java.lang.Object, java.lang.Object) */ @Override public synchronized boolean remove(IFile key, IFile value) { boolean removed = super.remove(key, value); if (removed) { if (!containsValue(value)) { _flags.remove(value); } } return removed; } /* (non-Javadoc) * @see org.eclipse.jdt.apt.core.internal.util.ManyToMany#removeKey(java.lang.Object) */ @Override public synchronized boolean removeKey(IFile key) { Set<IFile> values = getValues(key); boolean removed = super.removeKey(key); if (removed) { for (IFile value : values) { if (!containsValue(value)) { _flags.remove(value); } } } return removed; } /* (non-Javadoc) * @see org.eclipse.jdt.apt.core.internal.util.ManyToMany#removeValue(java.lang.Object) */ @Override public synchronized boolean removeValue(IFile value) { boolean removed = super.removeValue(value); if (removed) { _flags.remove(value); } return removed; } /** * Clear the file dependencies and delete the serialized state. * This will take effect even if the dirty bit is not set. */ public synchronized void clearState() { clear(); File state = getStateFile(_proj); if (state != null) { boolean successfullyDeleted = state.delete(); if (!successfullyDeleted && state.exists()) { AptPlugin.log(new IOException("Could not delete apt dependency state file"), //$NON-NLS-1$ state.getPath()); } } clearDirtyBit(); } /** * Convenience method, equivalent to put(key, value, [no flags]) */ @Override public synchronized boolean put(IFile parent, IFile generated) { return put(parent, generated, Collections.<Flags>emptySet()); } /** * Convenience method, equivalent to put(key, value, isSource ? [no flags] : [NONSOURCE]) */ public boolean put(IFile parent, IFile generated, boolean isSource) { return put(parent, generated, isSource ? Collections.<Flags>emptySet() : EnumSet.of(Flags.NONSOURCE)); } /** * Add a parent-to-generated association and specify attributes for the generated file. * The attributes are associated with the file, not the link: that is, a given generated * file can only have one set of attributes, not a different set per parent. The attributes * set in the most recent call will override those set in previous calls. */ public synchronized boolean put(IFile parent, IFile generated, Set<Flags> flags) { if (flags.isEmpty()) { _flags.remove(generated); } else { _flags.put(generated, flags); } return super.put(parent, generated); } public Set<Flags> getFlags(IFile generated) { Set<Flags> flags = _flags.get(generated); return flags == null ? Collections.<Flags>emptySet() : flags; } /** * Convenience method, equivalent to !getFlags(generated).contains(Flags.NONSOURCE) * @return true if the generated file is a source (Java) file rather than text, xml, etc. */ public boolean isSource(IFile generated) { return !getFlags(generated).contains(Flags.NONSOURCE); } /** * Utility method for serialization */ private String convertIFileToPath(IFile file) { IPath path = file.getProjectRelativePath(); return path.toOSString(); } /** * Utility method for deserialization */ private IFile convertPathToIFile(String projectRelativeString) { IPath path = new Path(projectRelativeString); return _proj.getFile(path); } /** * Returns the File to use for saving and restoring the last built state for the given project. * Returns null if the project does not exists (e.g. has been deleted) */ private File getStateFile(IProject project) { if (!project.exists()) return null; IPath workingLocation = project.getWorkingLocation(AptPlugin.PLUGIN_ID); return workingLocation.append("state.dat").toFile(); //$NON-NLS-1$ } /** * Reads the last serialized build state into memory. This includes dependency * information so that we do not need to do a clean build in order to recreate * our dependencies. * * File format: * * int version * int sizeOfMap * String parentIFilePath * int numberOfChildren * String childIFilePath * * This method is not synchronized because it is called only from this object's constructor. */ private void readState() { File file = getStateFile(_proj); if (file == null || !file.exists()) { // We'll just start with no dependencies return; } DataInputStream in = null; try { in= new DataInputStream(new BufferedInputStream(new FileInputStream(file))); int version = in.readInt(); if (version != SERIALIZATION_VERSION) { throw new IOException("Dependency map file version does not match. Expected " //$NON-NLS-1$ + SERIALIZATION_VERSION + ", but found " + version); //$NON-NLS-1$ } int sizeOfMap = in.readInt(); // For each entry, we'll have a parent and a set of children, // which we can drop into the parent -> child map. for (int parentIndex=0; parentIndex<sizeOfMap; parentIndex++) { String parentPath = in.readUTF(); IFile parent = convertPathToIFile(parentPath); int numChildren = in.readInt(); for (int childIndex = 0; childIndex<numChildren; childIndex++) { String childPath = in.readUTF(); IFile child = convertPathToIFile(childPath); // add the child to the parent->child map put(parent, child); } } // Now the _flags map: int sizeOfFlags = in.readInt(); for (int i = 0; i < sizeOfFlags; ++i) { String childPath = in.readUTF(); IFile child = convertPathToIFile(childPath); if (!containsValue(child)) { throw new IOException("Error in generated file attributes: did not expect file " + childPath); //$NON-NLS-1$ } int attributeCount = in.readInt(); EnumSet<Flags> flags = EnumSet.noneOf(Flags.class); for (int j = 0; j < attributeCount; ++j) { String attr = in.readUTF(); Flags f = Flags.valueOf(attr); flags.add(f); } _flags.put(child, flags); } // our serialized and in-memory states are now identical clearDirtyBit(); } catch (IOException ioe) { // Avoid partial initialization clear(); // We can safely continue without having read our dependencies. AptPlugin.logWarning(ioe, "Could not read APT dependencies: generated files may not be deleted until the next clean"); //$NON-NLS-1$ } catch (IllegalArgumentException iae) { // Avoid partial initialization clear(); // We can safely continue without having read our dependencies. AptPlugin.logWarning(iae, "Could not read APT dependencies: generated files may not be deleted until the next clean"); //$NON-NLS-1$ } finally { if (in != null) { try {in.close();} catch (IOException ioe) {} } } } /** * Write our dependencies to disk. If not dirty, nothing is written. */ public synchronized void writeState() { if (!isDirty()) { return; } File file = getStateFile(_proj); if (file == null) { // Cannot write state, as project has been deleted return; } file.delete(); DataOutputStream out = null; try { out = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file))); out.writeInt(SERIALIZATION_VERSION); // Number of parent files Set<IFile> parents = getKeySet(); out.writeInt(parents.size()); // for each parent... for (IFile parent : parents) { // ...parent name out.writeUTF(convertIFileToPath(parent)); Set<IFile> children = getValues(parent); // ...number of children out.writeInt(children.size()); // for each child... for (IFile child : children) { // ...child name. out.writeUTF(convertIFileToPath(child)); } } // Number of generated files with attributes out.writeInt(_flags.size()); // for each generated file that has attributes... for (Entry<IFile, Set<Flags>> entry : _flags.entrySet()) { // ...generated file name out.writeUTF(convertIFileToPath(entry.getKey())); Set<Flags> flags = entry.getValue(); // ...number of attributes out.writeInt(flags.size()); for (Flags f : flags) { // ...attribute name out.writeUTF(f.name()); } } // our serialized and in-memory states are now identical clearDirtyBit(); out.flush(); } catch (IOException ioe) { // We can safely continue without having written our dependencies. AptPlugin.logWarning(ioe, "Could not serialize APT dependencies"); //$NON-NLS-1$ } finally { if (out != null) { try { out.close(); } catch (IOException ioe) { // Do nothing } } } } }