/*******************************************************************************
* Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) and others.
* 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:
* Thomas Holland - initial API and implementation
*******************************************************************************/
package de.innot.avreclipse.core.avrdude;
import java.io.FileNotFoundException;
import java.net.URI;
import java.text.MessageFormat;
import java.util.List;
import org.eclipse.cdt.managedbuilder.core.IConfiguration;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
import de.innot.avreclipse.AVRPlugin;
import de.innot.avreclipse.core.properties.AVRDudeProperties;
import de.innot.avreclipse.core.toolinfo.fuses.ByteValues;
import de.innot.avreclipse.core.toolinfo.fuses.ByteValuesFactory;
import de.innot.avreclipse.core.toolinfo.fuses.ConversionResults;
import de.innot.avreclipse.core.toolinfo.fuses.FuseType;
import de.innot.avreclipse.mbs.BuildMacro;
/**
* Storage independent container for the Fuse and Lockbits Byte values.
* <p>
* This class is the bridge between the plugin property system and the {@link ByteValues} to
* actually hold the data.
* </p>
* <p>
* This class has two modes. Depending on the {@link #fUseFile} flag, it will either read the fuse
* values from a supplied file or immediate values stored in a byte values object. The mode is
* selected by the user in the Properties user interface.
* </p>
* <p>
* This class can be used either standalone or as part of the AVRProjectProperties structure.
* </p>
*
* @author Thomas Holland
* @since 2.2
*
*/
public abstract class BaseBytesProperties {
public final static int FILE_NOT_FOUND = 200;
public final static int FILE_MCU_PROPERTY_MISSING = 201;
public final static int FILE_WRONG_TYPE = 202;
public final static int FILE_INVALID_FILENAME = 203;
public final static int FILE_EMPTY_FILENAME = 204;
/** The MCU id for which the current fuse byte values are valid */
private String fMCUid;
private static final String KEY_MCUID = "MCUid";
/**
* Write flag
* <p>
* If <code>true</code>, the byte values are written to the target device when avrdude is
* executed.
* </p>
*/
private boolean fWriteFlag;
private final static String KEY_WRITEFLAG = "Write";
private final static boolean DEFAULT_WRITEFLAG = false;
/**
* Use file flag.
* <p>
* If <code>true</code>, the byte values are read from a file.
* </p>
* <p>
* If <code>false</code> the values from {@link #fByteValues} are used.
* </p>
*/
private boolean fUseFile;
private final static String KEY_USEFILE = "UseFile";
private final static boolean DEFAULT_USEFILE = false;
/**
* The name of the file.
* <p>
* This is used when the {@link #fUseFile} flag is <code>true</code>.
* </p>
* <p>
* The name can contain macros. They can be resolved by the caller or with the
* {@link #getFileNameResolved(IConfiguration)} method.
* </p>
*/
private String fFileName;
private final static String KEY_FILENAME = "FileName";
private final static String DEFAULT_FILENAME = "";
private ByteValues fFileByteValues = null;
/**
* The current byte values.
* <p>
* This is used when the {@link #fUseFile} flag is <code>false</code>.
* </p>
*/
private ByteValues fByteValues;
private final static String KEY_BYTEVALUES = "ByteValues";
private final static String SEPARATOR = ":";
/**
* The <code>Preferences</code> used to read / save the current properties.
*
*/
private final Preferences fPrefs;
/**
* The Parent <code>AVRDudeProperties</code>. Can be <code>null</code> if this class is used in
* stand alone mode.
*
*/
private final AVRDudeProperties fParent;
/** Build configuration used to resolve filenames. */
private IConfiguration fBuildConfig = null;
/** <code>true</code> if the properties have been modified and need saving. */
private boolean fDirty = false;
/**
* Create a new FuseBytesProperties object and load the properties from the Preferences.
* <p>
* If the given Preferences has no saved properties yet, the default values are used.
* </p>
*
* @param prefs
* <code>Preferences</code> to read the properties from.
* @param parent
* Reference to the <code>AVRDudeProperties</code> parent object.
*/
protected BaseBytesProperties(Preferences prefs, AVRDudeProperties parent) {
fPrefs = prefs;
fParent = parent;
fByteValues = new ByteValues(getType(), parent.getParent().getMCUId());
load();
}
/**
* Cloning constructor.
* <p>
* All values from the source are copied, except for the source Preferences and the Parent.
* </p>
*
* @param prefs
* <code>Preferences</code> to read the properties from.
* @param parent
* Reference to the <code>AVRDudeProperties</code> parent object.
* @param source
* <code>FuseBytesProperties</code> object to copy.
*/
public BaseBytesProperties(Preferences prefs, AVRDudeProperties parent,
BaseBytesProperties source) {
fPrefs = prefs;
fParent = parent;
fMCUid = source.fMCUid;
fWriteFlag = source.fWriteFlag;
fUseFile = source.fUseFile;
fFileName = source.fFileName;
fByteValues = new ByteValues(source.fByteValues);
}
/**
* Hook method for subclasses to supply the {@link FuseType} for the properties.
*
* @return Either <code>FuseType.FUSE</code> or <code>FuseType.LOCKBITS</code>
*/
protected abstract FuseType getType();
/**
* Get the MCU id value for which this object is valid.
*
* @return <code>String</code> with an mcu id. May be <code>null</code> if a non-existing file
* has been set as source.
*/
public String getMCUId() {
if (fUseFile) {
try {
return getByteValuesFromFile().getMCUId();
} catch (CoreException ce) {
// if the file does not exist or can not be opened we can not return a valid MCU
return null;
}
}
return fMCUid;
}
/**
* Tells this class that the current byte values are valid for the given MCU.
* <p>
* Use this method with care, as there will be no checks if the current values actually make
* sense for the new MCU type.
* </p>
* <p>
* The new setting is only valid for the internally stored values. If a file is used it is not
* affected and a call to {@link #getMCUId()} will return the mcu from the file, not this one.
* </p>
*
* @param mcuid
*/
public void setMCUId(String mcuid) {
if (!fMCUid.equals(mcuid)) {
fMCUid = mcuid;
// copy the old byte values to a new ByteValues Object for the given MCU
ByteValues newByteValues = new ByteValues(getType(), mcuid);
newByteValues.setValues(fByteValues.getValues());
fByteValues = newByteValues;
fDirty = true;
}
}
/**
* Get the "write to target MCU" flag.
*
* @return <code>true</code> if the byte values should be written to the target device when
* avrdude is executed.
*/
public boolean getWrite() {
return fWriteFlag;
}
/**
* Set the "write to target MCU" flag.
*
* @param enable
* <code>true</code> to enable writing the bytes managed by this class when avrdude
* is executed.
*/
public void setWrite(boolean enable) {
if (fWriteFlag != enable) {
fWriteFlag = enable;
fDirty = true;
}
}
/**
* Get the current value of the "Use File" flag.
*
* @see #setFileName(String)
* @see #getValue(int)
* @see #getValues()
*
* @return <code>true</code> if the byte values are taken from a file, <code>false</code> if the
* values stored in this object are used.
*/
public boolean getUseFile() {
return fUseFile;
}
/**
* Set the value of the "Use File" flag.
*
* @see #setFileName(String)
* @see #getValue(int)
* @see #getValues()
*
*
* @param usefile
* <code>true</code> if the fuse values should be read from the file,
* <code>false</code> if the values stored in this object are used.
*/
public void setUseFile(boolean usefile) {
if (fUseFile != usefile) {
fUseFile = usefile;
fDirty = true;
}
}
/**
* Get the current name of the file with all macros resolved.
* <p>
* Note: The returned path may still be OS independent and needs to be converted to an OS
* specific path (e.g. with <code>new Path(resolvedname).toOSString()</code>
* </p>
* <p>
* To resolve any macros this method needs an <code>IConfiguration</code> for the macro context.
* This needs to be set with the {@link #setBuildConfig(IConfiguration)} method. If no build
* configuration has been set, this method will return the filename unresolved.
* </p>
*
* @return <code>String</code> with the resolved filename. May be empty and may not point to an
* actual or valid file.
*/
public String getFileNameResolved() {
if (fBuildConfig != null) {
return BuildMacro.resolveMacros(fBuildConfig, fFileName);
}
return fFileName;
}
/**
* Sets the current build configuration.
* <p>
* This is only used to expand macros in the filename of an optional fuse/locks file. If it is
* <code>null</code> filenames can not be resolved and used as is.
* </p>
*
* @param config
* The current build configuration.
*/
public void setBuildConfig(IConfiguration config) {
fBuildConfig = config;
}
/**
* Get the current name of the file.
* <p>
* The returned string may still contain macros.
* </p>
*
* @return <code>String</code> with the name of the file. May be empty and may not point to an
* actual or valid file.
*/
public String getFileName() {
return fFileName;
}
/**
* Set the name of the file.
* <p>
* The given filename is stored as-is. There are no checks if the file is valid or even exists.
* </p>
*
* @param fusesfile
* <code>String</code> with the name of a file.
*/
public void setFileName(String filename) {
if (!fFileName.equals(filename)) {
fFileName = filename;
fFileByteValues = null;
fDirty = true;
}
}
/**
* Get all current byte values as a <code>ByteValues</code> object.
* <p>
* Get all bytes according to the current setting either from a file or from the object storage.
* <br>
* The returned <code>ByteValues</code> object is a copy of the internal values and any
* modifications are not reflected on the values in this object.
* </p>
* <p>
* To modify the values the use of the {@link #setValue(int, int)}, {@link #setValues(int[])}
* and {@link #setByteValues(ByteValues)} is required so this class can track any modifications
* and set the dirty flag as required.
* </p>
* <p>
* All values are either a valid bytes (0 - 255) or <code>-1</code> if no value was set.
* </p>
* <
*
* @return
*/
public ByteValues getByteValues() {
if (fUseFile) {
try {
return getByteValuesFromFile();
} catch (CoreException ce) {
return null;
}
}
return new ByteValues(fByteValues);
}
public ByteValues getByteValuesFromFile() throws CoreException {
if (fFileByteValues == null) {
// First get the IFile of the file and check that it exists
String rawfilename = getFileName();
if (rawfilename == null || rawfilename.length() == 0) {
String message = "Empty Filename";
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, FILE_EMPTY_FILENAME,
message, null);
throw new CoreException(status);
}
String filename = getFileNameResolved();
if (filename == null || filename.length() == 0) {
String message = MessageFormat.format("Invalid filename [{0}]", getFileName());
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID,
FILE_INVALID_FILENAME, message, new FileNotFoundException(filename));
throw new CoreException(status);
}
IPath location = new Path(filename);
IFile file = getFileFromLocation(location);
if (file == null || !file.exists()) {
String message = MessageFormat
.format("File not found [{0}]", location.toOSString());
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, FILE_NOT_FOUND,
message, new FileNotFoundException(file != null ? file.getFullPath()
.toOSString() : null));
throw new CoreException(status);
}
// then use the FuseFileDocumentProvider to get a ByteValues object for the file.
// The input file is immediately disconnected, because this class does not have a
// dispose method where the the disconnection could take place.
// This means that any changes to the file are not synchronized. Therefore the
// ByteValues object returned by this method should not be used for prolonged periods.
fFileByteValues = ByteValuesFactory.createByteValues(file);
// If the fFileByteValues are null, then the file could not be read.
// probably the 'MCU' Property is missing.
if (fFileByteValues == null) {
String message = MessageFormat.format(
"{0} is not a valid {1} file (probably the MCU=xxxx property is missing)",
file.getFullPath(), getType());
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID,
FILE_MCU_PROPERTY_MISSING, message, null);
throw new CoreException(status);
}
// Check if the file actually has the right type.
if (!getType().equals(fFileByteValues.getType())) {
// No! Discard the object and throw an Exception
String message = MessageFormat.format(
"{0} is a {1} file, but expected a {2} file.", file.getFullPath(),
fFileByteValues.getType(), getType());
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, FILE_WRONG_TYPE,
message, null);
fFileByteValues = null;
throw new CoreException(status);
}
}
return new ByteValues(fFileByteValues);
}
/**
* Sets the current byte values.
* <p>
* This method copies the given <code>ByteValues</code>, so that it cannot be changed without
* going through the methods of this class.
* </p>
* <p>
* The MCU Id of this class is set to the one of the given <code>ByteValues</code>
*
* @param newvalues
* The ByteValues object to copy from.
* @throws IllegalArgumentException
* if the type of the new byte values (FUSE or LOCKS) does not match the current
* setting.
*/
public void setByteValues(ByteValues newvalues) {
if (fUseFile) {
// TODO: handle files
return;
}
if (fByteValues.getType() != newvalues.getType()) {
throw new IllegalArgumentException("Cannot set a " + newvalues.getType().toString()
+ " ByteValues object");
}
fByteValues = new ByteValues(newvalues);
fMCUid = newvalues.getMCUId();
fDirty = true;
return;
}
public void setDefaultValues() {
fByteValues.setDefaultValues();
fDirty = true;
}
/**
* Get all current byte values as an array of <code>int</code>.
* <p>
* Get all bytes according to the current setting either from a file or from the object storage.
* </p>
* <p>
* All values are either a valid bytes (0 - 255) or <code>-1</code> if no value was set.
* </p>
*
* @return Array of <code>int</code> with all byte values.
*/
public int[] getValues() {
if (fUseFile) {
return getValuesFromFile();
}
return getValuesFromImmediate();
}
/**
* Get all current byte values from the file.
* <p>
* All values are either a valid bytes (0 - 255) or <code>-1</code> if no value was set.
* </p>
*
* @return Array of <code>int</code> with all byte values.
*/
public int[] getValuesFromFile() {
try {
ByteValues fileByteValues = getByteValuesFromFile();
return fileByteValues.getValues();
} catch (CoreException e) {
// File not found / not readable
return new int[] {};
}
}
/**
* Get all current byte values stored in the object.
* <p>
* All values are either a valid bytes (0 - 255) or <code>-1</code> if no value was set.
* </p>
*
* @return Array of <code>int</code> with all byte values.
*/
public int[] getValuesFromImmediate() {
return fByteValues.getValues();
}
/**
* Sets the values of all bytes in the object.
* <p>
* Even with the "Use File" flag set, this method will set the values stored in the object.
* </p>
*
* @see #setByteValue(int, int)
*
* @param values
* Array of <code>int</code> with the new values.
* @throws IllegalArgumentException
* if any value in the array is not a byte value or not <code>-1</code>
*/
public void setValues(int[] values) {
// While values[].length should be equal to the length of the internal
// field (and equal to MAX_FUSEBYTES), we use this to avoid any
// OutOfBoundExceptions
int min = Math.min(values.length, fByteValues.getByteCount());
// Set all individual values. setFuseValue() will take care of setting
// the dirty flag as needed.
for (int i = 0; i < min; i++) {
setValue(i, values[i]);
}
}
/**
* Get a single byte value.
* <p>
* Get the byte according to the current setting either from a file or from the object storage.
* </p>
*
* @param index
* The byte to read. Must be between 0 and <code>getMaxBytes() - 1</code>
* @return <code>int</code> with the byte value or <code>-1</code> if the value was not set or
* the index is out of bounds.
*/
public int getValue(int index) {
if (!(0 <= index && index < fByteValues.getByteCount())) {
return -1;
}
// getByteValues() will take care of the "use file" flag and
// return the relevant byte values.
int[] values = getValues();
return values[index];
}
/**
* Set a single byte value.
* <p>
* The value is always written to the object storage, regardless of the "Use File" flag.
* </p>
*
* @param index
* The byte to set. Must be between 0 and <code>getByteCount() - 1</code>, otherwise
* the value is ignored.
* @param value
* <code>int</code> with the byte value (0-255) or <code>-1</code> to unset the
* value.
* @throws IllegalArgumentException
* if the the value is out of range (-1 to 255)
*/
public void setValue(int index, int value) {
if (!(0 <= index && index < fByteValues.getByteCount())) {
return;
}
if (!(-1 <= value && value <= 255)) {
throw new IllegalArgumentException("invalid value:" + index
+ " (must be between 0 and 255)");
}
if (fByteValues.getValue(index) != value) {
fByteValues.setValue(index, value);
fDirty = true;
}
}
/**
* Clears all values.
* <p>
* This method will set the value of all bytes to <code>-1</code>
* </p>
*/
public void clearValues() {
fByteValues.clearValues();
fDirty = true;
}
/**
* Copies the byte values from the file to the object storage.
*/
public void syncFromFile() {
try {
ByteValues source = getByteValuesFromFile();
setByteValues(source);
} catch (CoreException ce) {
// do nothing if the file does not exist.
}
}
/**
* Get the list of avrdude arguments required to write all bytes.
* <p>
* Note: This method does <strong>not</strong> set the "-u" flag to disable the safemode. It is
* up to the caller to add this flag. If the "disable safemode" flag is not set, avrdude will
* restore the previous fusebyte values after the new values have been written.
* </p>
*
* @return <code>List<String></code> with avrdude action options.
*/
public abstract List<String> getArguments(String mcuid);
/**
* Load the properties from the Preferences.
* <p>
* The <code>Preferences</code> object used is set in the constructor of this class.
* </p>
*
*/
private void load() {
// Get the MCU id of the parent TargetProperties
// This is used as the default mcuid for this bytes object.
String parentmcuid = fParent.getParent().getMCUId();
fMCUid = fPrefs.get(KEY_MCUID, parentmcuid);
fWriteFlag = fPrefs.getBoolean(KEY_WRITEFLAG, DEFAULT_WRITEFLAG);
fUseFile = fPrefs.getBoolean(KEY_USEFILE, DEFAULT_USEFILE);
fFileName = fPrefs.get(KEY_FILENAME, DEFAULT_FILENAME);
String fusevaluestring = fPrefs.get(KEY_BYTEVALUES, "");
// Check if the mcu id is different than the parent. In that case we
// need a new ByteValues object
if (!(fMCUid.equals(parentmcuid))) {
// copy the old byte values to a new ByteValues Object for the given MCU
fByteValues = new ByteValues(getType(), fMCUid);
}
// Clear the old values
fByteValues.clearValues();
// split the values
String[] values = fusevaluestring.split(SEPARATOR);
int count = Math.min(values.length, fByteValues.getByteCount());
for (int i = 0; i < count; i++) {
String value = values[i];
if (value.length() != 0) {
fByteValues.setValue(i, Integer.parseInt(values[i]));
}
}
fDirty = false;
}
/**
* Save the current property values to the Preferences.
* <p>
* The <code>Preferences</code> object used is set in the constructor of this class.
* </p>
*
* @throws BackingStoreException
*/
public void save() throws BackingStoreException {
if (fDirty) {
fPrefs.put(KEY_MCUID, fMCUid);
fPrefs.putBoolean(KEY_WRITEFLAG, fWriteFlag);
fPrefs.putBoolean(KEY_USEFILE, fUseFile);
String filename = fFileName != null ? fFileName : "";
fPrefs.put(KEY_FILENAME, filename);
// convert the values to a single String
StringBuilder sb = new StringBuilder(20);
for (int i = 0; i < fByteValues.getByteCount(); i++) {
if (i > 0)
sb.append(SEPARATOR);
sb.append(fByteValues.getValue(i));
}
fPrefs.put(KEY_BYTEVALUES, sb.toString());
fPrefs.flush();
}
fDirty = false;
}
/**
* @return <code>true</code> if the object has unsaved changes
*/
public boolean isDirty() {
return fDirty;
}
/**
* Test if this Object is valid for the given MCU.
*
* @return <code>true</code> if the current byte values (either immediate or from a file) are
* valid for the given MCU id.
*
*/
public boolean isCompatibleWith(String mcuid) {
if (fUseFile) {
try {
ByteValues fileByteValues = getByteValuesFromFile();
return fileByteValues.isCompatibleWith(mcuid);
} catch (CoreException e) {
// Can't read the file
return false;
}
}
return fByteValues.isCompatibleWith(mcuid);
}
public void convertTo(String mcuid, ConversionResults results) {
if (fUseFile) {
// TODO: Check against the file
return;
}
fByteValues = fByteValues.convertTo(mcuid, results);
fMCUid = mcuid;
}
private IFile getFileFromLocation(IPath location) {
// Convert the IPath to an URI (findFilesForLocation(IPath) is deprecated)
URI locationAsURI = URIUtil.toURI(location);
IWorkspace workspace = ResourcesPlugin.getWorkspace();
IWorkspaceRoot root = workspace.getRoot();
try {
IFile[] files = root.findFilesForLocationURI(locationAsURI);
if (files.length != 0) {
return files[0];
}
} catch (IllegalArgumentException iae) {
// The caller will throw the appropriate FIleNotFound Exception
}
return null;
}
}