/*
* 02/21/2005
*
* CodeTemplateManager.java - manages code templates.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rsyntaxtextarea;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Segment;
import org.fife.ui.rsyntaxtextarea.templates.CodeTemplate;
/**
* Manages "code templates."<p>
*
* All methods in this class are synchronized for thread safety, but as a
* best practice, you should probably only modify the templates known to a
* <code>CodeTemplateManager</code> on the EDT. Modifying a
* <code>CodeTemplate</code> retrieved from a <code>CodeTemplateManager</code>
* while <em>not</em> on the EDT could cause problems.<p>
*
* For more flexible boilerplate code insertion, consider using the
* <a href="http://javadoc.fifesoft.com/autocomplete/org/fife/ui/autocomplete/TemplateCompletion.html">TemplateCompletion
* class</a> in the
* <a href="https://github.com/bobbylight/AutoComplete">AutoComplete
* add-on library</a>.
*
* @author Robert Futrell
* @version 1.0
*/
public class CodeTemplateManager {
private int maxTemplateIDLength;
private List<CodeTemplate> templates;
private Segment s;
private TemplateComparator comparator;
private File directory;
/**
* Constructor.
*/
public CodeTemplateManager() {
s = new Segment();
comparator = new TemplateComparator();
templates = new ArrayList<CodeTemplate>();
}
/**
* Registers the specified template with this template manager.
*
* @param template The template to register.
* @throws IllegalArgumentException If <code>template</code> is
* <code>null</code>.
* @see #removeTemplate(CodeTemplate)
* @see #removeTemplate(String)
*/
public synchronized void addTemplate(CodeTemplate template) {
if (template==null) {
throw new IllegalArgumentException("template cannot be null");
}
templates.add(template);
sortTemplates();
}
/**
* Returns the template that should be inserted at the current caret
* position, assuming the trigger character was pressed.
*
* @param textArea The text area that's getting text inserted into it.
* @return A template that should be inserted, if appropriate, or
* <code>null</code> if no template should be inserted.
*/
public synchronized CodeTemplate getTemplate(RSyntaxTextArea textArea) {
int caretPos = textArea.getCaretPosition();
int charsToGet = Math.min(caretPos, maxTemplateIDLength);
try {
Document doc = textArea.getDocument();
doc.getText(caretPos-charsToGet, charsToGet, s);
@SuppressWarnings("unchecked")
int index = Collections.binarySearch(templates, s, comparator);
return index>=0 ? (CodeTemplate)templates.get(index) : null;
} catch (BadLocationException ble) {
ble.printStackTrace();
throw new InternalError("Error in CodeTemplateManager");
}
}
/**
* Returns the number of templates this manager knows about.
*
* @return The template count.
*/
public synchronized int getTemplateCount() {
return templates.size();
}
/**
* Returns the templates currently available.
*
* @return The templates available.
*/
public synchronized CodeTemplate[] getTemplates() {
CodeTemplate[] temp = new CodeTemplate[templates.size()];
return templates.toArray(temp);
}
/**
* Returns whether the specified character is a valid character for a
* <code>CodeTemplate</code> id.
*
* @param ch The character to check.
* @return Whether the character is a valid template character.
*/
public static final boolean isValidChar(char ch) {
return RSyntaxUtilities.isLetterOrDigit(ch) || ch=='_';
}
/**
* Returns the specified code template.
*
* @param template The template to remove.
* @return <code>true</code> if the template was removed, <code>false</code>
* if the template was not in this template manager.
* @throws IllegalArgumentException If <code>template</code> is
* <code>null</code>.
* @see #removeTemplate(String)
* @see #addTemplate(CodeTemplate)
*/
public synchronized boolean removeTemplate(CodeTemplate template) {
if (template==null) {
throw new IllegalArgumentException("template cannot be null");
}
// TODO: Do a binary search
return templates.remove(template);
}
/**
* Returns the code template with the specified id.
*
* @param id The id to check for.
* @return The code template that was removed, or <code>null</code> if
* there was no template with the specified ID.
* @throws IllegalArgumentException If <code>id</code> is <code>null</code>.
* @see #removeTemplate(CodeTemplate)
* @see #addTemplate(CodeTemplate)
*/
public synchronized CodeTemplate removeTemplate(String id) {
if (id==null) {
throw new IllegalArgumentException("id cannot be null");
}
// TODO: Do a binary search
for (Iterator<CodeTemplate> i=templates.iterator(); i.hasNext(); ) {
CodeTemplate template = i.next();
if (id.equals(template.getID())) {
i.remove();
return template;
}
}
return null;
}
/**
* Replaces the current set of available templates with the ones
* specified.
*
* @param newTemplates The new set of templates. Note that we will
* be taking a shallow copy of these and sorting them.
*/
public synchronized void replaceTemplates(CodeTemplate[] newTemplates) {
templates.clear();
if (newTemplates!=null) {
for (int i=0; i<newTemplates.length; i++) {
templates.add(newTemplates[i]);
}
}
sortTemplates(); // Also recomputes maxTemplateIDLength.
}
/**
* Saves all templates as XML files in the current template directory.
*
* @return Whether or not the save was successful.
*/
public synchronized boolean saveTemplates() {
if (templates==null)
return true;
if (directory==null || !directory.isDirectory())
return false;
// Blow away all old XML files to start anew, as some might be from
// templates we're removed from the template manager.
File[] oldXMLFiles = directory.listFiles(new XMLFileFilter());
if (oldXMLFiles==null)
return false; // Either an IOException or it isn't a directory.
int count = oldXMLFiles.length;
for (int i=0; i<count; i++) {
/*boolean deleted = */oldXMLFiles[i].delete();
}
// Save all current templates as XML.
boolean wasSuccessful = true;
for (CodeTemplate template : templates) {
File xmlFile = new File(directory, template.getID() + ".xml");
try {
XMLEncoder e = new XMLEncoder(new BufferedOutputStream(
new FileOutputStream(xmlFile)));
e.writeObject(template);
e.close();
} catch (IOException ioe) {
ioe.printStackTrace();
wasSuccessful = false;
}
}
return wasSuccessful;
}
/**
* Sets the directory in which to look for templates. Calling this
* method adds any new templates found in the specified directory to
* the templates already registered.
*
* @param dir The new directory in which to look for templates.
* @return The new number of templates in this template manager, or
* <code>-1</code> if the specified directory does not exist.
*/
public synchronized int setTemplateDirectory(File dir) {
if (dir!=null && dir.isDirectory()) {
this.directory = dir;
File[] files = dir.listFiles(new XMLFileFilter());
int newCount = files==null ? 0 : files.length;
int oldCount = templates.size();
List<CodeTemplate> temp =
new ArrayList<CodeTemplate>(oldCount+newCount);
temp.addAll(templates);
for (int i=0; i<newCount; i++) {
try {
XMLDecoder d = new XMLDecoder(new BufferedInputStream(
new FileInputStream(files[i])));
Object obj = d.readObject();
if (!(obj instanceof CodeTemplate)) {
throw new IOException("Not a CodeTemplate: " +
files[i].getAbsolutePath());
}
temp.add((CodeTemplate)obj);
d.close();
} catch (/*IO, NoSuchElement*/Exception e) {
// NoSuchElementException can be thrown when reading
// an XML file not in the format expected by XMLDecoder.
// (e.g. CodeTemplates in an old format).
e.printStackTrace();
}
}
templates = temp;
sortTemplates();
return getTemplateCount();
}
return -1;
}
/**
* Removes any null entries in the current set of templates (if
* any), sorts the remaining templates, and computes the new
* maximum template ID length.
*/
private synchronized void sortTemplates() {
// Get the maximum length of a template ID.
maxTemplateIDLength = 0;
// Remove any null entries (should only happen because of
// IOExceptions, etc. when loading from files), and sort
// the remaining list.
for (Iterator<CodeTemplate> i=templates.iterator(); i.hasNext(); ) {
CodeTemplate temp = i.next();
if (temp==null || temp.getID()==null) {
i.remove();
}
else {
maxTemplateIDLength = Math.max(maxTemplateIDLength,
temp.getID().length());
}
}
Collections.sort(templates);
}
/**
* A comparator that takes a <code>CodeTemplate</code> as its first
* parameter and a <code>Segment</code> as its second, and knows
* to compare the template's ID to the segment's text.
*/
@SuppressWarnings("rawtypes")
private static class TemplateComparator implements Comparator, Serializable{
public int compare(Object template, Object segment) {
// Get template start index (0) and length.
CodeTemplate t = (CodeTemplate)template;
final char[] templateArray = t.getID().toCharArray();
int i = 0;
int len1 = templateArray.length;
// Find "token" part of segment and get its offset and length.
Segment s = (Segment)segment;
char[] segArray = s.array;
int len2 = s.count;
int j = s.offset + len2 - 1;
while (j>=s.offset && isValidChar(segArray[j])) {
j--;
}
j++;
int segShift = j - s.offset;
len2 -= segShift;
int n = Math.min(len1, len2);
while (n-- != 0) {
char c1 = templateArray[i++];
char c2 = segArray[j++];
if (c1 != c2)
return c1 - c2;
}
return len1 - len2;
}
}
/**
* A file filter that accepts only XML files.
*/
private static class XMLFileFilter implements FileFilter {
public boolean accept(File f) {
return f.getName().toLowerCase().endsWith(".xml");
}
}
}