/*
Copyright (C) 2011 Jason von Nieda <jason@vonnieda.org>
This file is part of OpenPnP.
OpenPnP 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.
OpenPnP 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 OpenPnP. If not, see <http://www.gnu.org/licenses/>.
For more information about OpenPnP visit http://openpnp.org
*/
package org.openpnp.model;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.prefs.Preferences;
import org.apache.commons.io.FileUtils;
import org.openpnp.ConfigurationListener;
import org.openpnp.spi.Machine;
import org.openpnp.util.ResourceUtils;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.convert.AnnotationStrategy;
import org.simpleframework.xml.core.Persister;
import org.simpleframework.xml.stream.Format;
import org.simpleframework.xml.stream.HyphenStyle;
import org.simpleframework.xml.stream.Style;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Configuration extends AbstractModelObject {
private static final Logger logger = LoggerFactory.getLogger(Configuration.class);
private static Configuration instance;
private static final String PREF_UNITS = "Configuration.units";
private static final String PREF_UNITS_DEF = "Millimeters";
private static final String PREF_LENGTH_DISPLAY_FORMAT = "Configuration.lengthDisplayFormat";
private static final String PREF_LENGTH_DISPLAY_FORMAT_DEF = "%.3f";
private static final String PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS = "Configuration.lengthDisplayFormatWithUnits";
private static final String PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS_DEF = "%.3f%s";
private static final String PREF_VERTICAL_SCROLL_UNIT_INCREMENT = "Configuration.verticalScrollUnitIncrement";
private static final int PREF_VERTICAL_SCROLL_UNIT_INCREMENT_DEF = 16;
private LinkedHashMap<String, Package> packages = new LinkedHashMap<String, Package>();
private LinkedHashMap<String, Part> parts = new LinkedHashMap<String, Part>();
private Machine machine;
private LinkedHashMap<File, Board> boards = new LinkedHashMap<File, Board>();
private boolean dirty;
private boolean loaded;
private Set<ConfigurationListener> listeners = Collections.synchronizedSet(new HashSet<ConfigurationListener>());
private File configurationDirectory;
private Preferences prefs;
public static Configuration get() {
if (instance == null) {
throw new Error("Configuration instance not yet initialized.");
}
return instance;
}
public static synchronized void initialize(File configurationDirectory) {
instance = new Configuration(configurationDirectory);
instance.setLengthDisplayFormatWithUnits(PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS_DEF);
}
private Configuration(File configurationDirectory) {
this.configurationDirectory = configurationDirectory;
this.prefs = Preferences.userNodeForPackage(Configuration.class);
}
public File getConfigurationDirectory() {
return configurationDirectory;
}
public LengthUnit getSystemUnits() {
return LengthUnit.valueOf(prefs.get(PREF_UNITS, PREF_UNITS_DEF));
}
public void setSystemUnits(LengthUnit lengthUnit) {
prefs.put(PREF_UNITS, lengthUnit.name());
}
public String getLengthDisplayFormat() {
return prefs.get(PREF_LENGTH_DISPLAY_FORMAT, PREF_LENGTH_DISPLAY_FORMAT_DEF);
}
public void setLengthDisplayFormat(String format) {
prefs.put(PREF_LENGTH_DISPLAY_FORMAT, format);
}
public String getLengthDisplayFormatWithUnits() {
return prefs.get(PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS, PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS_DEF);
}
public void setLengthDisplayFormatWithUnits(String format) {
prefs.put(PREF_LENGTH_DISPLAY_FORMAT_WITH_UNITS, format);
}
public int getVerticalScrollUnitIncrement() {
return prefs.getInt(PREF_VERTICAL_SCROLL_UNIT_INCREMENT, PREF_VERTICAL_SCROLL_UNIT_INCREMENT_DEF);
}
public void setVerticalScrollUnitIncrement(int verticalScrollUnitIncrement) {
prefs.putInt(PREF_VERTICAL_SCROLL_UNIT_INCREMENT, PREF_VERTICAL_SCROLL_UNIT_INCREMENT_DEF);
}
/**
* Gets a File reference for the resources directory belonging to the
* given class. The directory is guaranteed to exist.
* @param forClass
* @return
* @throws IOException
*/
public File getResourceDirectory(Class forClass) throws IOException {
File directory = new File(configurationDirectory, forClass.getCanonicalName());
if (!directory.exists()) {
directory.mkdirs();
}
return directory;
}
/**
* Gets a File reference for the named file within the configuration
* directory. forClass is used to uniquely identify the file and keep it
* separate from other classes' files.
* @param forClass
* @param name
* @return
*/
public File getResourceFile(Class forClass, String name) throws IOException {
return new File(getResourceDirectory(forClass), name);
}
/**
* Creates a new file with a unique name within the configuration
* directory. forClass is used to uniquely identify the file within
* the application and a unique name is generated within that namespace.
* suffix is appended to the unique part of the filename. The result of
* calling File.getName() on the returned file can be used to load the
* same file in the future by calling getResourceFile().
* This method uses File.createTemporaryFile() and so the rules for that
* method must be followed when calling this one.
* @param forClass
* @param suffix
* @return
* @throws IOException
*/
public File createResourceFile(Class forClass, String prefix, String suffix) throws IOException {
File directory = new File(configurationDirectory, forClass.getCanonicalName());
if (!directory.exists()) {
directory.mkdirs();
}
File file = File.createTempFile(prefix, suffix, directory);
return file;
}
public boolean isDirty() {
return dirty;
}
public void setDirty(boolean dirty) {
this.dirty = dirty;
}
public void addListener(ConfigurationListener listener) {
listeners.add(listener);
if (loaded) {
try {
listener.configurationLoaded(this);
listener.configurationComplete(this);
}
catch (Exception e) {
// TODO: Need to find a way to raise this to the GUI
throw new Error(e);
}
}
}
public void removeListener(ConfigurationListener listener) {
listeners.remove(listener);
}
public void load() throws Exception {
boolean forceSave = false;
boolean overrideUserConfig = Boolean.getBoolean("overrideUserConfig");
try {
File file = new File(configurationDirectory, "packages.xml");
if (overrideUserConfig || !file.exists()) {
logger.info("No packages.xml found in configuration directory, loading defaults.");
file = File.createTempFile("packages", "xml");
FileUtils.copyURLToFile(ClassLoader.getSystemResource("config/packages.xml"), file);
forceSave = true;
}
loadPackages(file);
}
catch (Exception e) {
String message = e.getMessage();
if (e.getCause() != null && e.getCause().getMessage() != null) {
message = e.getCause().getMessage();
}
throw new Exception("Error while reading packages.xml (" + message + ")", e);
}
try {
File file = new File(configurationDirectory, "parts.xml");
if (overrideUserConfig || !file.exists()) {
logger.info("No parts.xml found in configuration directory, loading defaults.");
file = File.createTempFile("parts", "xml");
FileUtils.copyURLToFile(ClassLoader.getSystemResource("config/parts.xml"), file);
forceSave = true;
}
loadParts(file);
}
catch (Exception e) {
String message = e.getMessage();
if (e.getCause() != null && e.getCause().getMessage() != null) {
message = e.getCause().getMessage();
}
throw new Exception("Error while reading parts.xml (" + message + ")", e);
}
try {
File file = new File(configurationDirectory, "machine.xml");
if (overrideUserConfig || !file.exists()) {
logger.info("No machine.xml found in configuration directory, loading defaults.");
file = File.createTempFile("machine", "xml");
FileUtils.copyURLToFile(ClassLoader.getSystemResource("config/machine.xml"), file);
forceSave = true;
}
loadMachine(file);
}
catch (Exception e) {
String message = e.getMessage();
if (e.getCause() != null && e.getCause().getMessage() != null) {
message = e.getCause().getMessage();
}
throw new Exception("Error while reading machine.xml (" + message + ")", e);
}
loaded = true;
for (ConfigurationListener listener : listeners) {
listener.configurationLoaded(this);
}
if (forceSave) {
logger.info("Defaults were loaded. Saving to configuration directory.");
configurationDirectory.mkdirs();
save();
}
for (ConfigurationListener listener : listeners) {
listener.configurationComplete(this);
}
}
public void save() throws Exception {
try {
saveMachine(new File(configurationDirectory, "machine.xml"));
}
catch (Exception e) {
throw new Exception("Error while saving machine.xml (" + e.getMessage() + ")", e);
}
try {
savePackages(new File(configurationDirectory, "packages.xml"));
}
catch (Exception e) {
throw new Exception("Error while saving packages.xml (" + e.getMessage() + ")", e);
}
try {
saveParts(new File(configurationDirectory, "parts.xml"));
}
catch (Exception e) {
throw new Exception("Error while saving parts.xml (" + e.getMessage() + ")", e);
}
dirty = false;
}
public Package getPackage(String id) {
if (id == null) {
return null;
}
return packages.get(id.toUpperCase());
}
public List<Package> getPackages() {
return Collections.unmodifiableList(new ArrayList<Package>(packages.values()));
}
public void addPackage(Package pkg) {
if (null == pkg.getId()) {
throw new Error("Package with null Id cannot be added to Configuration.");
}
packages.put(pkg.getId().toUpperCase(), pkg);
dirty = true;
firePropertyChange("packages", null, packages);
}
public Part getPart(String id) {
if (id == null) {
return null;
}
return parts.get(id.toUpperCase());
}
public List<Part> getParts() {
return Collections.unmodifiableList(new ArrayList<Part>(parts.values()));
}
public void addPart(Part part) {
if (null == part.getId()) {
throw new Error("Part with null Id cannot be added to Configuration.");
}
parts.put(part.getId().toUpperCase(), part);
dirty = true;
firePropertyChange("parts", null, parts);
}
public List<Board> getBoards() {
return Collections.unmodifiableList(new ArrayList<Board>(boards.values()));
}
public Machine getMachine() {
return machine;
}
public Board getBoard(File file) throws Exception {
if (!file.exists()) {
Board board = new Board(file);
board.setName(file.getName());
Serializer serializer = createSerializer();
serializer.write(board, file);
}
file = file.getCanonicalFile();
if (boards.containsKey(file)) {
return boards.get(file);
}
Board board = loadBoard(file);
boards.put(file, board);
firePropertyChange("boards", null, boards);
return board;
}
private void loadMachine(File file) throws Exception {
Serializer serializer = createSerializer();
MachineConfigurationHolder holder = serializer.read(MachineConfigurationHolder.class, file);
machine = holder.machine;
}
private void saveMachine(File file) throws Exception {
MachineConfigurationHolder holder = new MachineConfigurationHolder();
holder.machine = machine;
Serializer serializer = createSerializer();
serializer.write(holder, new ByteArrayOutputStream());
serializer.write(holder, file);
}
private void loadPackages(File file) throws Exception {
Serializer serializer = createSerializer();
PackagesConfigurationHolder holder = serializer.read(PackagesConfigurationHolder.class, file);
for (Package pkg : holder.packages) {
addPackage(pkg);
}
}
private void savePackages(File file) throws Exception {
Serializer serializer = createSerializer();
PackagesConfigurationHolder holder = new PackagesConfigurationHolder();
holder.packages = new ArrayList<Package>(packages.values());
serializer.write(holder, new ByteArrayOutputStream());
serializer.write(holder, file);
}
private void loadParts(File file) throws Exception {
Serializer serializer = createSerializer();
PartsConfigurationHolder holder = serializer.read(PartsConfigurationHolder.class, file);
for (Part part : holder.parts) {
addPart(part);
}
}
private void saveParts(File file) throws Exception {
Serializer serializer = createSerializer();
PartsConfigurationHolder holder = new PartsConfigurationHolder();
holder.parts = new ArrayList<Part>(parts.values());
serializer.write(holder, new ByteArrayOutputStream());
serializer.write(holder, file);
}
public Job loadJob(File file) throws Exception {
Serializer serializer = createSerializer();
Job job = serializer.read(Job.class, file);
job.setFile(file);
// Once the Job is loaded we need to resolve any Boards that it
// references.
for (BoardLocation boardLocation : job.getBoardLocations()) {
String boardFilename = boardLocation.getBoardFile();
// First see if we can find the board at the given filename
// If the filename is not absolute this will be relative
// to the working directory
File boardFile = new File(boardFilename);
if (!boardFile.exists()) {
// If that fails, see if we can find it relative to the
// directory the job was in
boardFile = new File(file.getParentFile(), boardFilename);
}
if (!boardFile.exists()) {
throw new Exception("Board file not found: " + boardFilename);
}
Board board = getBoard(boardFile);
boardLocation.setBoard(board);
}
job.setDirty(false);
return job;
}
public void saveJob(Job job, File file) throws Exception {
Serializer serializer = createSerializer();
Set<Board> boards = new HashSet<Board>();
// Fix the paths to any boards in the Job
for (BoardLocation boardLocation : job.getBoardLocations()) {
Board board = boardLocation.getBoard();
boards.add(board);
try {
String relativePath = ResourceUtils.getRelativePath(
board.getFile().getAbsolutePath(),
file.getAbsolutePath(),
File.separator);
boardLocation.setBoardFile(relativePath);
}
catch (ResourceUtils.PathResolutionException ex) {
boardLocation.setBoardFile(board.getFile().getAbsolutePath());
}
}
// Save the job
serializer.write(job, new ByteArrayOutputStream());
serializer.write(job, file);
job.setFile(file);
job.setDirty(false);
}
public void saveBoard(Board board) throws Exception {
Serializer serializer = createSerializer();
serializer.write(board, new ByteArrayOutputStream());
serializer.write(board, board.getFile());
board.setDirty(false);
}
private Board loadBoard(File file) throws Exception {
Serializer serializer = createSerializer();
Board board = serializer.read(Board.class, file);
board.setFile(file);
board.setDirty(false);
return board;
}
public static Serializer createSerializer() {
Style style = new HyphenStyle();
Format format = new Format(style);
AnnotationStrategy strategy = new AnnotationStrategy();
Serializer serializer = new Persister(strategy, format);
return serializer;
}
/**
* Used to provide a fixed root for the Machine when serializing.
*/
@Root(name="openpnp-machine")
public static class MachineConfigurationHolder {
@Element
private Machine machine;
}
/**
* Used to provide a fixed root for the Packages when serializing.
*/
@Root(name="openpnp-packages")
public static class PackagesConfigurationHolder {
@ElementList(inline=true, entry="package", required=false)
private ArrayList<Package> packages = new ArrayList<Package>();
}
/**
* Used to provide a fixed root for the Parts when serializing.
*/
@Root(name="openpnp-parts")
public static class PartsConfigurationHolder {
@ElementList(inline=true, entry="part", required=false)
private ArrayList<Part> parts = new ArrayList<Part>();
}
}