/*******************************************************************************
* 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.ui.editors;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.IElementStateListener;
import de.innot.avreclipse.AVRPlugin;
import de.innot.avreclipse.core.toolinfo.fuses.BitFieldDescription;
import de.innot.avreclipse.core.toolinfo.fuses.ByteValueChangeEvent;
import de.innot.avreclipse.core.toolinfo.fuses.ByteValues;
import de.innot.avreclipse.core.toolinfo.fuses.FuseType;
import de.innot.avreclipse.core.toolinfo.fuses.IByteValuesChangeListener;
/**
* Connect a Fuses file document to a ByteValues object.
*
* @author Thomas Holland
* @since 2.3
*
*/
public class DocumentByteValuesConnector {
/** Reference to the parent DocumentProvider to add and remove Element State Change Listener. */
private final IDocumentProvider fProvider;
/** Source of the Document. Used to set / clear problem markers. */
private IFile fSource;
/** The source document */
private final IDocument fDocument;
/** The ByteValues created from and synchronized with the source IDocument. */
private ByteValues fByteValues = null;
/**
* <code>true</code> while the source IDocument is modified from this class, so the document
* change listener can ignore the resulting change events.
*/
private boolean fInDocumentChange = false;
/**
* <code>true</code> while the ByteValues are modified from this class, so the ByteValues change
* listener can ignore the resulting change events.
*/
private boolean fInByteValuesChange = false;
private final IDocumentListener fDocumentListener;
private final IByteValuesChangeListener fByteValuesListener;
private final IElementStateListener fElementStateListener;
/** The current value of a key. */
private final Map<String, String> fKeyValueMap = new HashMap<String, String>();
/** The current linenumber of a key. */
private final Map<String, Integer> fKeyLineMap = new HashMap<String, Integer>();
/** The <code>Position</code> of a key in the document. */
private final Map<String, Position> fKeyPositionMap = new HashMap<String, Position>();
/** The <code>Position</code> of a key value in the document. */
private final Map<String, Position> fKeyValuePositionMap = new HashMap<String, Position>();
/** MCU property key string. */
private final static String KEY_MCU = "MCU";
/**
* Comment property key string. Called 'summary' to be compatible with AVR32 Studio file format.
*/
private final static String KEY_COMMENT = "summary";
/** RegEx pattern for comment lines. Comments are all lines starting with "#". */
private final static Pattern fCommentPattern = Pattern.compile("\\s*#.*");
/** RegEx pattern for property lines. Properties match the "key=value" pattern. */
private final static Pattern fPropertyPattern = Pattern
.compile("\\s*(\\w*)\\s*=(.*)");
// ------------ IDocumentListener -------------------
/**
* Listener to listen for Document change events.
* <p>
* Whenever the document is changed, e.g. by the TextEditor, the document is parsed and the
* associated ByteValues object is updated.
* </p>
*/
private class MyDocumentListener implements IDocumentListener {
/*
* (non-Javadoc)
* @see
* org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text
* .DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent event) {
// ignore event
}
/*
* (non-Javadoc)
* @see
* org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent
* )
*/
public void documentChanged(DocumentEvent event) {
if (fInDocumentChange) {
// don't listen to events we generated ourself
return;
}
// This is sub-optimal (but easy):
// For each modification of the document we parse the complete document again.
// Not very efficient, but the fuses files are small and even on my
// old and slow Notebook this takes less than one millisecond.
updateByteValuesFromDocument();
}
}
// ------------ IByteValuesChangedListener -------------------
/**
* Listener for ByteValues change events.
* <p>
* Whenever the <code>ByteValues</code> object has been changed, e.g. by the form editor, the
* document is updated accordingly.
* </p>
*/
private class MyByteValuesChangedListener implements IByteValuesChangeListener {
/*
* (non-Javadoc)
* @see
* de.innot.avreclipse.core.toolinfo.fuses.IByteValuesChangeListener#byteValuesChanged(de
* .innot.avreclipse.core.toolinfo.fuses.ByteValueChangeEvent[])
*/
public void byteValuesChanged(ByteValueChangeEvent[] events) {
if (fInByteValuesChange) {
// don't listen to events we generated ourself
return;
}
for (ByteValueChangeEvent event : events) {
String key = event.name;
if (key.equals(ByteValues.MCU_CHANGE_EVENT)) {
// If the MCU has changed we clear the document and rewrite it completely
clearDocument();
updateDocumentFromByteValues(fByteValues);
} else if (key.equals(ByteValues.COMMENT_CHANGE_EVENT)) {
// The comment has changed
String comment = fByteValues.getComment();
setDocumentComment(comment);
} else {
// a single value has changed. Update the property or remove it if the value has
// become undefined (-1)
if (event.bytevalue != -1) {
setDocumentValue(key, event.bitfieldvalue);
} else {
removeDocumentValue(key);
}
}
}
}
}
// ---- DocumentProvider Element Change Listener Methods ------
/**
* Listener for Element State change events.
* <p>
* This is used to listen for move / rename events to track the source file, so that problem
* markers can be set and removed.
* </p>
*
*/
private class MyElementStateListener implements IElementStateListener {
public void elementMoved(Object originalElement, Object movedElement) {
if (originalElement == null) {
return;
}
// If our source file has moved, we need to store the new file, so that we can create
// new problem markers for it. Existing problem markers are automatically moved by the
// Workbench.
IFile originalfile = getFileFromAdaptable(originalElement);
IFile movedfile = getFileFromAdaptable(movedElement);
if (fSource.equals(originalfile)) {
fSource = movedfile;
}
}
public void elementDeleted(Object element) {
// Nothing to do
// Even if the source file has been deleted, the Document and therefore the ByteValues
// object is still valid and it might be saved under a different name.
}
public void elementContentAboutToBeReplaced(Object element) {
// Nothing to do
}
public void elementContentReplaced(Object element) {
// Nothing to do
}
public void elementDirtyStateChanged(Object element, boolean isDirty) {
// Nothing to do
}
}
/**
* Create a new DocumentByteValuesConnector.
* <p>
* The new connector takes the given document and registers as a listener to all changes of the
* document. The synchronized ByteValues object is created lazily with the getByteValues() or
* setByteValues() methods.
* </p>
* <p>
* The connector also registers itself as a listener to the provider to be informed if the
* source file is moved or renamed. The source element, which needs to be adaptable to
* <code>IFile</code> is used to determine the type of the ByteValues (FUSE or LOCKBITS) via the
* file extension. The file is also needed to create the <code>IMarker</code>s for all problems
* parsing the file.
* </p>
* <p>
* The connector needs to be disposed when it is not needed anymore to remove the listeners.
* </p>
*
* @param provider
* An <code>IDocumentProvider</code>
* @param document
* The source <code>IDocument</code>
* @param element
* The source file as an object that can be adapted to an <code>IFile</code>, e.g. an
* <code>IFileEditorInput</code>
* @throws CoreException
* if the element is not adaptable to <code>IFile</code>.
*/
public DocumentByteValuesConnector(IDocumentProvider provider, IDocument document,
Object element) throws CoreException {
// Note: With the provider and the source element we could determine the document ourself.
// But then we would either depend on the caller to connect the source element for us, or
// connect ourself and risk infinite loops as the constructor is called from the connect()
// method of the FuseFileDocumentProvider.
fSource = getFileFromAdaptable(element);
if (fSource == null) {
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID,
"Object must be an IFile", null);
throw new CoreException(status);
}
// Create the listener objects
fDocumentListener = new MyDocumentListener();
fByteValuesListener = new MyByteValuesChangedListener();
fElementStateListener = new MyElementStateListener();
// and add the listener to the provider and the document
fProvider = provider;
fProvider.addElementStateListener(fElementStateListener);
fDocument = document;
fDocument.addDocumentListener(fDocumentListener);
// The ByteValues object will be created lazily, i.e. when actually requested with the
// getByteValues() method.
fByteValues = null;
}
// ------- Public Methods -------
/**
* Disposes the Connector.
* <p>
* This method will remove the document, provider and ByteValues listeners. After calling this
* method changes to either the document or the ByteValues are not synchronized anymore.
* </p>
*/
public void dispose() {
fDocument.removeDocumentListener(fDocumentListener);
fProvider.removeElementStateListener(fElementStateListener);
if (fByteValues != null) {
fByteValues.removeChangeListener(fByteValuesListener);
}
}
/**
* Connects the given <code>ByteValues</code> to the source Document / File, copying all its
* values to the document.
* <p>
* The new values object replaces any previously generated values object. The previous values
* object is disconnected and will not be updated anymore.
* </p>
*
* @param newvalues
* A <code>ByteValues</code> object to connect to the source document. Must not be
* <code>null</code>.
*/
public void setByteValues(ByteValues newvalues) {
Assert.isNotNull(newvalues);
updateDocumentFromByteValues(newvalues);
if (fByteValues != null) {
fByteValues.removeChangeListener(fByteValuesListener);
}
fByteValues = newvalues;
fByteValues.addChangeListener(fByteValuesListener);
}
/**
* @return A <code>ByteValues</code> object connected to the source document.
*/
public ByteValues getByteValues() {
if (fByteValues == null) {
fByteValues = createByteValues();
updateByteValuesFromDocument();
}
return fByteValues;
}
// ------- Private Methods -------
/**
* Create a new ByteValues object from the source document.
*
* @return Valid <code>ByteValues</code> object or <code>null</code> if either the source file
* has an unknown extension or there is 'MCU' property tag in the source document.
*/
private ByteValues createByteValues() {
FuseType type = null;
try {
type = getTypeFromFileExtension(fSource);
} catch (CoreException ce) {
// Exception is thrown if the file extension is neither ".fuses" or ".locks".
// This should not happen, so we log the message and return null.
AVRPlugin.getDefault().log(ce.getStatus());
return null;
}
parseDocument();
String mcuid = fKeyValueMap.get(KEY_MCU);
if (mcuid == null) {
setMissingMCUMarker();
return null;
}
ByteValues newvalues = new ByteValues(type, mcuid);
newvalues.addChangeListener(fByteValuesListener);
if (newvalues.getByteCount() == 0) {
// unknown MCU
setIllegalValueMarker("MCU", mcuid);
} else {
clearMarker("MCU");
}
return newvalues;
}
/**
* Parse the source document and copy all applicable properties to the <code>ByteValues</code>
* object.
*/
private void updateByteValuesFromDocument() {
if (fByteValues == null) {
// no need to waste cycles until the ByteValues have been created.
return;
}
parseDocument();
fInByteValuesChange = true;
for (String key : fKeyValueMap.keySet()) {
String value = fKeyValueMap.get(key);
if ("MCU".equalsIgnoreCase(key)) {
String oldmcuid = fByteValues.getMCUId();
if (!oldmcuid.equals(value)) {
fByteValues.setMCUId(value, false);
// All previously set BitFields might have changed
// so just restart from the beginning.
// On the second iteration the MCUs will match, so no
// danger of recursion.
updateByteValuesFromDocument();
return;
}
if (fByteValues.getByteCount() == 0) {
// unknown MCU
setIllegalValueMarker("MCU", value);
} else {
clearMarker("MCU");
}
continue;
}
if ("summary".equalsIgnoreCase(key)) {
String comment = value.replace("\\n", "\n");
fByteValues.setComment(comment);
continue;
}
if (fByteValues.getBitFieldDescription(key) == null) {
setInvalidKeyMarker(key);
continue;
}
try {
int intvalue = Integer.decode(fKeyValueMap.get(key));
fByteValues.setNamedValue(key, intvalue);
clearMarker(key);
} catch (NumberFormatException nfe) {
setIllegalValueMarker(key, value);
} catch (IllegalArgumentException iae) {
setIllegalValueMarker(key, value);
}
}
fInByteValuesChange = false;
}
/**
* Clears the document (removing all non-comment lines) and writes all values from the
* <code>ByteValues</code> object to the document.
*
* @param newvalues
*/
private void updateDocumentFromByteValues(ByteValues newvalues) {
clearDocument();
setDocumentValue(KEY_MCU, newvalues.getMCUId());
List<BitFieldDescription> bfdlist = newvalues.getBitfieldDescriptions();
for (BitFieldDescription bfd : bfdlist) {
String key = bfd.getName();
int value = newvalues.getNamedValue(key);
if (value == -1) {
continue;
}
setDocumentValue(key, value);
}
String comment = newvalues.getComment();
setDocumentComment(comment);
}
/**
* Sets the comment property of the document.
* <p>
* This method converts the given comment to a single line form, escaping all new line
* characters.
* </p>
*
* @param comment
* The new comment, may be <code>null</code>
*/
private void setDocumentComment(String comment) {
String newcomment = comment==null?"":comment;
// Escape all new line characters. The comment must stay on one line because the parser only
// works with single lines.
newcomment = newcomment.replace("\r\n", "\\n");
newcomment = newcomment.replace("\n", "\\n");
newcomment = newcomment.replace("\r", "\\n");
setDocumentValue(KEY_COMMENT, newcomment);
}
/**
* Sets the document property with the given key to a new integer value. The value is converted
* to a hex string and prepended with "0x".
*
* @param key
* The property key
* @param value
* The new integer value
*/
private void setDocumentValue(String key, int value) {
String textvalue = "0x" + Integer.toHexString(value);
setDocumentValue(key, textvalue);
}
/**
* Sets the document property with the given key to a new string value.
*
* @param key
* The property key
* @param value
* The new string value. Must not contain new line characters but can be
* <code>null</code>
*/
private void setDocumentValue(String key, String value) {
try {
int offset;
int length;
String text;
Integer linenumber = fKeyLineMap.get(key);
if (linenumber == null) {
// Key does not exist yet - add it.
offset = fDocument.getLength();
length = 0;
text = key + "=" + (value == null ? "" : value) + "\n";
fInDocumentChange = true;
fDocument.replace(offset, length, text);
linenumber = fDocument.getLineOfOffset(offset + 4);
} else {
// Key exists - replace value;
Position position = fKeyValuePositionMap.get(key);
offset = position.getOffset();
length = position.getLength();
text = value == null ? "" : value;
fInDocumentChange = true;
fDocument.replace(offset, length, text);
// remove the key from all maps so that it can be re-read
fKeyValueMap.remove(key);
fKeyLineMap.remove(key);
fDocument.removePosition(fKeyPositionMap.get(key));
fDocument.removePosition(fKeyValuePositionMap.get(key));
fKeyPositionMap.remove(key);
fKeyValuePositionMap.remove(key);
}
// (re)read the line to update the internal maps
parseLine(linenumber);
clearMarker(key);
} catch (BadLocationException ble) {
// This exception probably means that there is a bug in the code above, in other words
// it should not happen.
// The Exception is logged, but otherwise we ignore it. It should be thrown, but then we
// would have to throw it all the way up to the ByteValues.setValue() method, breaking
// lots of stuff on the way.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Bug in setDocumentValue(). Please contact the plugin author.", ble);
AVRPlugin.getDefault().log(status);
// Because our stored meta information about the document might have become foul we
// parse the document again just in case
parseDocument();
} finally {
fInDocumentChange = false;
}
}
/**
* Remove the line containing the given key from the document.
* <p>
* If the key does not exist in the document nothing is changed.
* </p>
*
* @param key
* The key which is to be completely removed from the document.
*/
private void removeDocumentValue(String key) {
// This method is called from the ByteValues change listener
// when a BitField has been set to -1, i.e. it has been undefined.
try {
Integer linenumber = fKeyLineMap.get(key);
if (linenumber == null) {
// document did not contain the key.
// do nothing and return
return;
}
// first remove the key from all lists
fKeyValueMap.remove(key);
fKeyLineMap.remove(key);
fDocument.removePosition(fKeyPositionMap.get(key));
fDocument.removePosition(fKeyValuePositionMap.get(key));
fKeyPositionMap.remove(key);
fKeyValuePositionMap.remove(key);
clearMarker(key);
// then update the list of lines, moving all lines behind the one to remove up by one.
for (String otherkey : fKeyLineMap.keySet()) {
int otherline = fKeyLineMap.get(otherkey);
if (otherline > linenumber) {
fKeyLineMap.put(otherkey, otherline - 1);
}
}
// finally remove the line from the document
IRegion lineregion = fDocument.getLineInformation(linenumber);
String delimiter = fDocument.getLineDelimiter(linenumber);
int offset = lineregion.getOffset();
int length = lineregion.getLength() + delimiter.length();
fInDocumentChange = true;
fDocument.replace(offset, length, null);
} catch (BadLocationException ble) {
// This exception probably means that there is a bug in the code above, in other words
// it should not happen.
// The Exception is logged, but otherwise we ignore it. It should be thrown, but then we
// would have to throw it all the way up to the ByteValues.setValue() method, breaking
// lots of stuff on the way.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Bug in removeDocumentValue(). Please contact the plugin author.", ble);
AVRPlugin.getDefault().log(status);
// Because our stored meta information about the document might have become foul we
// parse the document again just in case
parseDocument();
} finally {
fInDocumentChange = false;
}
}
/**
* Remove all lines with valid properties from the document. After calling this method only the
* comments and lines without an '=' remain.
*/
private void clearDocument() {
clearAllMarkers();
// remove all lines with keys in them
for (String key : fKeyPositionMap.keySet()) {
Position position = fKeyPositionMap.get(key);
if (!position.isDeleted) {
try {
int line = fDocument.getLineOfOffset(position.offset);
int offset = fDocument.getLineOffset(line);
int length = fDocument.getLineLength(line);
fInDocumentChange = true;
fDocument.replace(offset, length, null);
} catch (BadLocationException ble) {
// This exception probably means that there is a bug in the code above, in other
// words it should not happen.
// The Exception is logged, but otherwise we ignore it.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Bug in clearDocument(). Please contact the plugin author.", ble);
AVRPlugin.getDefault().log(status);
} finally {
fInDocumentChange = false;
}
}
}
fKeyLineMap.clear();
fKeyPositionMap.clear();
fKeyValueMap.clear();
fKeyValuePositionMap.clear();
}
/**
* Create an error marker to inform the user that the document has no valid "MCU" property.
*/
private void setMissingMCUMarker() {
String message = MessageFormat.format("Required Property '{0}' missing", KEY_MCU);
createMarker(KEY_MCU, IMarker.SEVERITY_ERROR, -1, 0, 0, message);
}
/**
* Create a warning marker to inform the user that a property key was not valid.
*
* @param key
* Property key
*/
private void setInvalidKeyMarker(String key) {
Position keyPosition = fKeyPositionMap.get(key);
int start = keyPosition.getOffset();
int end = start + keyPosition.getLength();
int linenumber = fKeyLineMap.get(key);
String message = MessageFormat.format("Invalid BitField name '{0}'", key);
createMarker(key, IMarker.SEVERITY_WARNING, linenumber, start, end, message);
}
/**
* Create a warning marker to inform the user that a property has an invalid value.
*
* @param key
* Property key
* @param value
* the invalid value
*/
private void setIllegalValueMarker(String key, String value) {
int linenumber = fKeyLineMap.get(key);
Position valuePosition = fKeyValuePositionMap.get(key);
int start = valuePosition.getOffset();
int end = start + valuePosition.getLength();
String message = MessageFormat.format("{0}: Invalid value [{1}]", key, value);
createMarker(key, IMarker.SEVERITY_WARNING, linenumber, start, end, message);
}
/**
* Create a warning marker to inform the user that there is a duplicate key in the file.
*
* @param key
* The duplicate key
* @param linenumber
* The line number of the duplicate
* @param start
* Start offset of the duplicate key in the document
* @param end
* End offset of the duplicate key in the document
*/
private void setDuplicateKeyMarker(String key, int linenumber, int start, int end) {
String message = MessageFormat.format("Duplicate BitField name {0}", key);
createMarker(null, IMarker.SEVERITY_WARNING, linenumber, start, end, message);
}
/**
* Creates a new <code>IMarker</code> and sets the given attibutes.
*
* @param key
* The property key of the marker. Used to find and remove the marker once the
* problem has been solved. May be <code>null</code>
* @param severity
* One of the IMarker.SEVERITY_xxx levels
* @param linenumber
* Linenumber of the Problem, or <code>-1</code> if the problem is not bound to a
* single line
* @param start
* The offset in the document where the problem starts
* @param end
* The offset in the document where the problem ends.
* @param message
* A human readable description of the problem
*/
private void createMarker(String key, int severity, int linenumber, int start, int end,
String message) {
if (fSource.exists()) {
try {
IMarker marker = fSource.createMarker(IMarker.PROBLEM);
marker.setAttribute(IMarker.SEVERITY, severity);
marker.setAttribute(IMarker.LINE_NUMBER, linenumber + 1);
marker.setAttribute(IMarker.CHAR_START, start);
marker.setAttribute(IMarker.CHAR_END, end);
marker.setAttribute(IMarker.MESSAGE, message);
marker.setAttribute(IMarker.SOURCE_ID, key);
return;
} catch (CoreException ce) {
// ignore the exception -> no marker created
}
}
}
/**
* Removes all markers that may exist for a given key.
* <p>
* This method is called when the parser determines that a line is completely valid.
* </p>
*
* @param key
*/
private void clearMarker(String key) {
if (fSource.exists()) {
// find all markers with the SOURCE_ID with the given key
try {
IMarker[] allmarkers = fSource.findMarkers(IMarker.PROBLEM, true,
IResource.DEPTH_INFINITE);
for (IMarker marker : allmarkers) {
String markerkey = marker.getAttribute(IMarker.SOURCE_ID, "");
if (markerkey.equals(key)) {
marker.delete();
}
}
} catch (CoreException ce) {
// This Exception would be thrown if the resource does not exist (but we check that
// it exists) or when the project is not open. The first case should not happen, the
// latter case is just logged but otherwise ignored.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Could not clear marker for key '" + key + "'", ce);
AVRPlugin.getDefault().log(status);
}
}
}
/**
* Clear all markers associated with the source file.
*/
private void clearAllMarkers() {
if (fSource.exists()) {
try {
fSource.deleteMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
} catch (CoreException ce) {
// This Exception would be thrown if the resource does not exist (but we check that
// it exists), when Resource changes are not allowed, or the project is not open.
// The first two cases should not happen and the last case is just logged but
// otherwise ignored.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Could not clear markers", ce);
AVRPlugin.getDefault().log(status);
}
}
}
/**
* Parses the source document.
* <p>
* Parses the document line by line. Once this method has finished, all internal metadata about
* the source document is up to date.
* </p>
*
*/
private void parseDocument() {
int lines = fDocument.getNumberOfLines();
// Clear all maps
fKeyLineMap.clear();
fKeyPositionMap.clear();
fKeyValuePositionMap.clear();
fKeyValueMap.clear();
clearAllMarkers();
try {
for (int linenumber = 0; linenumber < lines; linenumber++) {
parseLine(linenumber);
}
} catch (BadLocationException ble) {
// This exception probably means that there is a bug in the parseLine() method, in other
// words it should not happen.
// The Exception is logged, but otherwise we ignore it.
IStatus status = new Status(IStatus.WARNING, AVRPlugin.PLUGIN_ID,
"Bug in parseLine(). Please contact the plugin author.", ble);
AVRPlugin.getDefault().log(status);
}
}
/**
* Parses a single line of the source document.
* <p>
* Reads the line with the given line number from the source document and determines if it is
* either a comment, a valid key/value pair, an empty line or an invalid line. In the last case
* a problem marker is created immediatley.
* </p>
* <p>
* If it is a key/value pair, then its information is added to the internal document metadata,
* like the value or the <code>Position</code> of both the key and the value.
* </p>
*
* @param linenumber
* @throws BadLocationException
*/
private void parseLine(int linenumber) throws BadLocationException {
IRegion lineregion = fDocument.getLineInformation(linenumber);
int offset = lineregion.getOffset();
int length = lineregion.getLength();
String linecontent = fDocument.get(offset, length);
Matcher matcher;
// Test if valid property line
matcher = fPropertyPattern.matcher(linecontent);
if (matcher.matches()) {
String key = matcher.group(1);
String value = matcher.group(2).trim();
if (fKeyValueMap.containsKey(key)) {
// duplicate key -> marks as error
setDuplicateKeyMarker(key, linenumber, offset + matcher.start(1), offset
+ matcher.end(1));
}
fKeyValueMap.put(key, value);
fKeyLineMap.put(key, linenumber);
int keyoffset = offset + matcher.start(1);
int keylength = offset + matcher.end(1) - keyoffset;
fKeyPositionMap.put(key, addPosition(keyoffset, keylength));
int valueoffset = offset + matcher.start(2);
int valuelength = offset + matcher.end(2) - valueoffset;
fKeyValuePositionMap.put(key, addPosition(valueoffset, valuelength));
return;
}
// Test if Comment
matcher = fCommentPattern.matcher(linecontent);
if (matcher.matches()) {
return;
}
// Test if empty line
if (linecontent.trim().length() == 0) {
return;
}
createMarker(null, IMarker.SEVERITY_WARNING, linenumber, offset, offset + length,
"Undefined line");
return;
}
/**
* Creates a new <code>Position</code> object and adds it to the source document.
* <p>
* The new position is tracked by the document and updated whenever document changes affect the
* Position.
* </p>
*
* @param offset
* start of the position range
* @param length
* number of chars in the position range
* @return New <code>Position</code> object.
* @throws BadLocationException
* if the given range is not contained within the source document.
*/
private Position addPosition(int offset, int length) throws BadLocationException {
Position position = new Position(offset, length);
fDocument.addPosition(position);
return position;
}
/**
* Gets the <code>FuseType</code> from the extension of a file.
*
* @param file
* File with a valid file extension
* @return Either {@link FuseType#FUSE} or {@link FuseType#LOCKBITS}
* @throws CoreException
* if the given file has no or an unrecognized extension.
*/
private FuseType getTypeFromFileExtension(IFile file) throws CoreException {
// First get the type of file from the extension
String extension = file.getFileExtension();
if (FuseType.FUSE.getExtension().equalsIgnoreCase(extension)) {
return FuseType.FUSE;
} else if (FuseType.LOCKBITS.getExtension().equalsIgnoreCase(extension)) {
return FuseType.LOCKBITS;
} else {
IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, "File ["
+ file.getFullPath().toOSString() + "] has an unrecognized extension.", null);
throw new CoreException(status);
}
}
/**
* Get an <code>IFile</code> from an adaptable element.
* <p>
* In the normal use of this class the object element will be an <code>IFileEditorInput</code>,
* but other adaptable objects would be accepted if they are adaptable to an <code>IFile</code>.
* </p>
*
* @param element
* an <code>IAdaptable</code> object that can be adapted to an <code>IFile</code>.
* @return the <code>IFile</code> from the given element or <code>null</code> if the element
* could not be adapted.
*/
private IFile getFileFromAdaptable(Object element) {
if (element instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) element;
IFile file = (IFile) adaptable.getAdapter(IFile.class);
return file;
}
return null;
}
}