public static class Application {
	public static final String NAME = "Sleeksnap";
	public static final String URL = "http://sleeksnap.com";
	public static final String UPDATE_URL = "http://sleeksnap.com/build/";
} public static class Resources {
	public static final String LOGO_PATH = "/logo.png";
	public static final String ICON_PATH = "/icon.png";
	public static final String ICON_BUSY_PATH = "/icon-busy.png";
		
	public static final URL ICON = Util.getResourceByName(Resources.ICON_PATH);
	public static final URL ICON_BUSY = Util.getResourceByName(Resources.ICON_BUSY_PATH);
		
	public static final Image ICON_IMAGE = Toolkit.getDefaultToolkit().getImage(ICON);
	public static final Image ICON_BUSY_IMAGE = Toolkit.getDefaultToolkit().getImage(ICON_BUSY);
} public static class Version {
	public static final int MAJOR = 1;
	public static final int MINOR = 4;
	public static final int PATCH = 9;
		
	private static String versionString = null;
		
	public static String getVersionString() {
		if(versionString == null) {
			// Construct string normally
			versionString = MAJOR + "." + MINOR + "." + PATCH;
		}
		return  versionString;
	}
} @SuppressWarnings("serial")
public class FilterException extends Exception {

	/**
	 * The separate error message, this is NOT the same as e.getMessage()
	 */
	private String errorMessage;

	public FilterException(Exception e) {
		super(e);
	}
	
	public FilterException(Exception e, String errorMessage) {
		super(e);
		this.errorMessage = errorMessage;
	}
	
	public String getErrorMessage() {
		return errorMessage;
	}

} public class PNGCompressionFilter implements UploadFilter<ImageUpload> {
	
	/**
	 * Logger object
	 */
	private static final Logger logger = Logger.getLogger(PNGCompressionFilter.class.getName());

	/**
	 * Location of the pngcrush program
	 * pngcrush is faster than pngout.
	 * Download: http://pmt.sourceforge.net/pngcrush/
	 */
	private static File pngCrushWindows = new File(Util.getWorkingDirectory(), "tools/pngcrush.exe"), pngCrushUnix = new File(Util.getWorkingDirectory(), "tools/pngcrush");
	
	/**
	 * Location of the pngout program
	 * pngout is slower than pngcrush, but has higher compression ratios.
	 * Download: http://advsys.net/ken/util/pngout.exe
	 */
	private static File pngOutWindows = new File(Util.getWorkingDirectory(), "tools/pngout.exe"), pngOutUnix = new File(Util.getWorkingDirectory(), "tools/pngcrush");
	
	private ScreenSnapper parent;
	
	public PNGCompressionFilter(ScreenSnapper parent) {
		this.parent = parent;
	}
	
	@Override
	public ImageUpload filter(ImageUpload object) throws FilterException {
		if(parent.getConfiguration().getBoolean("compressImages")) {
			File pngOut = null;
			File pngCrush = null;
			if(Platform.isWindows()) {
				pngCrush = pngCrushWindows;
				pngOut = pngOutWindows;
			} else {
				pngCrush = pngCrushUnix;
				pngOut = pngOutUnix;
			}
			if(pngOut.exists() || pngCrush.exists()) {
				try {
					//TODO verify it is a png image.
					File input = new File(Util.getWorkingDirectory(), "sleeksnap_original.png");
					File output = new File(Util.getWorkingDirectory(), "sleeksnap_compressed.png");
					
					ImageIO.write(object.getImage(), "png", input);
					
					String[] opts = new String[3];
					opts[0] = strPad((pngOut.exists() ? pngOut : pngCrush).getAbsolutePath(), '"');
					opts[1] = strPad(input.getAbsolutePath(), '"');
					opts[2] = strPad(output.getAbsolutePath(), '"');
					
					logger.info("Compressing image with "+(pngOut.exists() ? "pngout" : "pngcrush") +"...");
					
					Process p = Runtime.getRuntime().exec(opts);
					BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
					
					while(reader.readLine() != null) {
						//Nothing.
					}
					
					logger.info("Compressed image, original size: "+input.length()+", compressed size: "+output.length());
					
					object.setImage(ImageIO.read(output));
					
					try {
						//Finally, read the new file.
						return object;
					} finally {
						input.delete();
						output.delete();
					}
				} catch (IOException e) {
					throw new FilterException(e, "Unable to compress image");
				}
			}
		}
		return object;
	}

	private static String strPad(String string, char pad) {
		return pad + string + pad;
	}
} public interface UploadFilter<T extends Upload> {

	/**
	 * Apply this filter to the object
	 * @param object
	 * 			The object to apply to
	 * @return
	 * 			The modified object, or original object.
	 * @throws FilterException 
	 */
	public T filter(T object) throws FilterException;
	
} public class WatermarkFilter implements UploadFilter<ImageUpload> {
	
	/**
	 * The watermark file, if it exists it will be applied if the image is large enough to have it not get in the way.
	 */
	private static final File watermarkFile = new File(Util.getWorkingDirectory(), "watermark.png");

	@Override
	public ImageUpload filter(ImageUpload object) {
		if(watermarkFile.exists()) {
			BufferedImage image = object.getImage();
			try {
				BufferedImage watermark = ImageIO.read(watermarkFile);
				if(image.getWidth() >= watermark.getWidth() && image.getHeight() >= watermark.getHeight()) {
					Graphics g = image.getGraphics();
					g.drawImage(watermark, image.getWidth()-watermark.getWidth(), image.getHeight()-watermark.getHeight(), watermark.getWidth(), watermark.getHeight(), null);
					g.dispose();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return object;
	}
} @SuppressWarnings({ "serial" })
public class OptionPanel extends JPanel {

	private ScreenSnapper snapper;

	private JTabbedPane jTabbedPane1;

	private int previousTab = 0;

	private InfoPanel infoPanel;

	private UploaderPanel uploaderPanel;

	private HotkeyPanel hotkeyPanel;

	private HistoryPanel historyPanel;

	private LogPanel logPanel;

	public OptionPanel(ScreenSnapper snapper) {
		this.snapper = snapper;
		initComponents();
	}

	public void doneBuilding() {
		infoPanel.doneBuilding();
		uploaderPanel.doneBuilding();
		hotkeyPanel.doneBuilding();
		historyPanel.doneBuilding();
		logPanel.doneBuilding();
	}

	private void initComponents() {

		jTabbedPane1 = new JTabbedPane();

		// New panels
		infoPanel = new InfoPanel(this);
		infoPanel.initComponents();

		uploaderPanel = new UploaderPanel(this);
		uploaderPanel.initComponents();

		hotkeyPanel = new HotkeyPanel(this);
		hotkeyPanel.initComponents();

		historyPanel = new HistoryPanel(this);
		historyPanel.initComponents();

		logPanel = new LogPanel(this);
		logPanel.initComponents();

		setMinimumSize(new java.awt.Dimension(500, 300));

		jTabbedPane1.setTabPlacement(JTabbedPane.LEFT);
		jTabbedPane1.setCursor(new java.awt.Cursor(
				java.awt.Cursor.DEFAULT_CURSOR));

		jTabbedPane1.addTab("Main", infoPanel);

		jTabbedPane1.addTab("Uploaders", uploaderPanel);

		jTabbedPane1.addTab("Hotkeys", hotkeyPanel);

		jTabbedPane1.addTab("History", historyPanel);

		jTabbedPane1.addTab("Log", logPanel);
		
		initializeTab("Updater", UpdaterPanel.class);

		GroupLayout layout = new GroupLayout(this);
		this.setLayout(layout);
		layout.setHorizontalGroup(layout.createParallelGroup(
				GroupLayout.Alignment.LEADING).addComponent(jTabbedPane1,
				GroupLayout.PREFERRED_SIZE, 520, GroupLayout.PREFERRED_SIZE));
		layout.setVerticalGroup(layout.createParallelGroup(
				GroupLayout.Alignment.LEADING).addComponent(jTabbedPane1,
				GroupLayout.PREFERRED_SIZE, 470, GroupLayout.PREFERRED_SIZE));

		jTabbedPane1.addChangeListener(new ChangeListener() {
			@Override
			public void stateChanged(ChangeEvent e) {
				int index = jTabbedPane1.getSelectedIndex();
				if (index == 4) {
					// Load the log
					File file = new File(Util.getWorkingDirectory(), "log.txt");
					try {
						String contents = StreamUtils
								.readContents(new FileInputStream(file));
						logPanel.setContents(contents);
					} catch (IOException ex) {
						ex.printStackTrace();
					}
				}
				if (previousTab == 2) {
					// Restart our input manager if we disabled it
					if (!snapper.getKeyManager().hasKeysBound()) {
						snapper.getKeyManager().initializeInput();
					}
				}
				previousTab = index;
			}
		});

		// Do any loading/initializing
		hotkeyPanel.loadCurrentHotkeys();
	}
	
	public void initializeTab(String name, Class<?> cl) {
		try {
			Constructor<?> c = cl.getConstructor(OptionPanel.class);
			if(c == null) {
				throw new NoSuchMethodException("Unable to find a valid constructor!");
			}
			
			OptionSubPanel panel = (OptionSubPanel) c.newInstance(this);
			
			panel.initComponents();
			
			jTabbedPane1.addTab(name, panel);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void setHistory(History history) {
		historyPanel.setHistory(history);
	}

	public UploaderPanel getUploaderPanel() {
		return uploaderPanel;
	}

	public ScreenSnapper getSnapper() {
		return snapper;
	}

	public Configuration getConfiguration() {
		return snapper.getConfiguration();
	}

	public void saveAll() {
		hotkeyPanel.savePreferences();
		uploaderPanel.savePreferences();
	}
} @SuppressWarnings("serial")
/**
 * A <code>javax.swing.JWindow</code> which allows us to push an image onto it for selection
 * 
 * @author Nikki
 *
 */
public class SelectionWindow extends JWindow {

	/**
	 * A <code>javax.swing.event.MouseInputAdapter</code> which listens for
	 * selections
	 * 
	 * @author Nikki
	 * 
	 */
	private class ScreenSelectionListener extends MouseInputAdapter {
		public void mouseDragged(MouseEvent e) {
			updateSize(e);
		}

		public void mousePressed(MouseEvent e) {
			int x = e.getX();
			int y = e.getY();
			if (e.getButton() == MouseEvent.BUTTON3) {
				e.consume();
				close();
				return;
			}
			currentRect = new Rectangle(x, y, 0, 0);
			updateDrawableRect(getWidth(), getHeight());
			repaint();
		}

		public void mouseReleased(MouseEvent e) {
			// Cancel button
			if (e.getButton() == MouseEvent.BUTTON3) {
				e.consume();
				close();
				return;
			}
			updateSize(e);
			try {
				capture();
			} catch (Exception ex) {
				ex.printStackTrace();
			}
		}

		/*
		 * Update the size of the current rectangle and call repaint. Because
		 * currentRect always has the same origin, translate it if the width or
		 * height is negative.
		 * 
		 * For efficiency (though that isn't an issue for this program), specify
		 * the painting region using arguments to the repaint() call.
		 */
		void updateSize(MouseEvent e) {
			currentRect.setSize(e.getX() - currentRect.x, e.getY()
					- currentRect.y);
			updateDrawableRect(getWidth(), getHeight());
			repaint();
		}
	}

	private Graphics g;
	private BufferedImage image;
	private Rectangle area;
	private Rectangle currentRect;
	private Rectangle rectToDraw = null;

	private ScreenSnapper snapper;

	private Image buffer;

	public SelectionWindow(ScreenSnapper snapper, Rectangle area) {
		this.snapper = snapper;
		this.area = area;
		this.image = ScreenshotUtil.capture(area);
		this.setPreferredSize(new Dimension(area.width, area.height));
		this.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
		this.setBounds(area);

		ScreenSelectionListener listener = new ScreenSelectionListener();
		addMouseListener(listener);
		addMouseMotionListener(listener);
	}

	public void capture() {
		Image sub = createImage(new FilteredImageSource(image.getSource(),
				new CropImageFilter(rectToDraw.x, rectToDraw.y,
						rectToDraw.width, rectToDraw.height)));
		if (sub == null) {
			throw new RuntimeException("Unable to crop!");
		}
		snapper.upload(new ImageUpload(ImageUtil.toBufferedImage(sub)));
		sub.flush();
		sub = null;
		close();
	}

	public void close() {
		dispose();
		snapper.clearWindow();
	}
	
	private static Color rectColor = new Color(0, 0, 0, 50);

	public void paint() {
		g.clearRect(area.x, area.y, area.width, area.height);
		g.drawImage(image, 0, 0, null);

		if (currentRect != null) {
			g.setColor(rectColor);
			g.drawRect(rectToDraw.x, rectToDraw.y, rectToDraw.width,
					rectToDraw.height);
			g.fillRect(rectToDraw.x, rectToDraw.y, rectToDraw.width,
					rectToDraw.height);
		}
	}

	@Override
	public void paint(Graphics gr) {
		if (buffer == null) {
			this.buffer = createImage(area.width, area.height);
			this.g = buffer.getGraphics();
		}
		// paint to the buffer
		paint();
		// draw the buffer
		gr.drawImage(buffer, 0, 0, this);
	}

	private void updateDrawableRect(int compWidth, int compHeight) {
		int x = currentRect.x;
		int y = currentRect.y;
		int width = currentRect.width;
		int height = currentRect.height;

		// Make the width and height positive, if necessary.
		if (width < 0) {
			width = 0 - width;
			x = x - width + 1;
			if (x < 0) {
				width += x;
				x = 0;
			}
		}
		if (height < 0) {
			height = 0 - height;
			y = y - height + 1;
			if (y < 0) {
				height += y;
				y = 0;
			}
		}

		// The rectangle shouldn't extend past the drawing area.
		if ((x + width) > compWidth) {
			width = compWidth - x;
		}
		if ((y + height) > compHeight) {
			height = compHeight - y;
		}

		// Update rectToDraw after saving old value.
		if (rectToDraw != null) {
			rectToDraw.setBounds(x, y, width, height);
		} else {
			rectToDraw = new Rectangle(x, y, width, height);
		}
	}
} public class History {
	
	/**
	 * The History Gson instance. It is different because we need a different Date serializer to keep the old history files working.
	 */
	private Gson gson = new GsonBuilder().registerTypeAdapter(Date.class, new DateSerializer()).create();

	/**
	 * The history storage class
	 */
	private List<HistoryEntry> history = new LinkedList<HistoryEntry>();

	/**
	 * The file to load/save to
	 */
	private File file;

	/**
	 * Initialize a new instance with the specified file
	 * 
	 * @param file
	 *            The file to use for storage
	 */
	public History(File file) {
		this.file = file;
	}

	/**
	 * Add an entry, forcing a save
	 * 
	 * @param entry
	 *            The entry
	 * @throws IOException
	 *             Thrown if an error occurred while saving, notify that local
	 *             history is disabled?
	 * @throws JSONException 
	 */
	public void addEntry(HistoryEntry entry) throws IOException {
		addEntry(entry, true);
	}

	/**
	 * Add an entry
	 * 
	 * @param entry
	 *            The entry
	 * @param save
	 *            True if a save is required
	 * @throws IOException
	 *             Thrown if an error occurred while saving, notify that local
	 *             history is disabled?
	 * @throws JSONException 
	 */
	public void addEntry(HistoryEntry entry, boolean save) throws IOException {
		synchronized (history) {
			history.add(entry);
			if (save) {
				save();
			}
		}
	}

	/**
	 * Get the history list
	 * 
	 * @return The list
	 */
	public List<HistoryEntry> getHistory() {
		return history;
	}

	/**
	 * Load the history from the specified file
	 * 
	 * @throws IOException
	 *             If an error occurs reading the file
	 */
	public void load() throws IOException {
		Reader reader = new FileReader(file);
		try {
			history = gson.fromJson(reader, new TypeToken<LinkedList<HistoryEntry>>() { }.getType());
		} finally {
			reader.close();
		}
	}

	/**
	 * Save the history to a file
	 * 
	 * @throws IOException
	 *             If an error occurs saving the file
	 */
	private void save() throws IOException {
		Writer writer = new FileWriter(file);
		try {
			gson.toJson(history, writer);
		} finally {
			writer.close();
		}
	}
	
	/**
	 * A temporary serialization class to work with the old HistoryEntry JSON Files
	 * 
	 * @author Nikki
	 *
	 */
	public class DateSerializer implements JsonSerializer<Date>, JsonDeserializer<Date> {

		@Override
		public Date deserialize(JsonElement element, Type type, JsonDeserializationContext context) throws JsonParseException {
			return new Date(element.getAsLong());
		}

		@Override
		public JsonElement serialize(Date date, Type type, JsonSerializationContext context) {
			return new JsonPrimitive(date.getTime());
		}
		
	}
} public class HistoryEntry {

	/**
	 * The date format used for the toString function
	 */
	private static DateFormat format = new SimpleDateFormat(
			"E, MMM d yyyy hh:mm:ss a");

	/**
	 * The uploaded file URL
	 */
	private String url;

	/**
	 * The name of the uploader used
	 */
	private String uploader;

	/**
	 * The date/time uploaded
	 */
	private Date date;

	/**
	 * A blank constructor... used for serialization beans
	 */
	public HistoryEntry() {

	}

	/**
	 * Constructs a new HistoryEntry object with the specified URL and uploader,
	 * and a new date object
	 * 
	 * @param url
	 *            The upload url
	 * @param uploader
	 *            The uploader name
	 */
	public HistoryEntry(String url, String uploader) {
		this(url, uploader, new Date());
	}

	/**
	 * Constructs a new HistoryEntry object with the specified URL, uploader and
	 * date
	 * 
	 * @param url
	 *            The URL
	 * @param uploader
	 *            The uploader name
	 * @param date
	 *            The date that the entry was made
	 */
	public HistoryEntry(String url, String uploader, Date date) {
		this.url = url;
		this.uploader = uploader;
		this.date = date;
	}

	/**
	 * Get the upload date
	 * 
	 * @return The date
	 */
	public Date getDate() {
		return date;
	}

	/**
	 * Get the uploader name
	 * 
	 * @return The uploader name
	 */
	public String getUploader() {
		return uploader;
	}

	/**
	 * Get the uploaded file URL
	 * 
	 * @return The URL
	 */
	public String getUrl() {
		return url;
	}

	/**
	 * Set the date
	 * 
	 * @param date
	 *            The date object to set it to
	 */
	public void setDate(Date date) {
		this.date = date;
	}

	/**
	 * Set the uploader name
	 * 
	 * @param uploader
	 *            The uploader name
	 */
	public void setUploader(String uploader) {
		this.uploader = uploader;
	}

	/**
	 * Set the URL
	 * 
	 * @param url
	 *            The URL
	 */
	public void setUrl(String url) {
		this.url = url;
	}

	@Override
	public String toString() {
		return format.format(date) + " - " + url;
	}
} public class HotkeyManager {
	
	/**
	 * Full screenshot hotkeys
	 */
	public static final String FULL_HOTKEY = "PRINTSCREEN", FULL_HOTKEY_MAC = "ctrl shift 1";
	
	/**
	 * Crop shot hotkeys
	 */
	public static final String CROP_HOTKEY = "ctrl PRINTSCREEN", CROP_HOTKEY_MAC = "ctrl shift 2";
	
	/**
	 * Active window hotkeys, for Linux and Windows only
	 */
	public static final String ACTIVE_HOTKEY = "alt PRINTSCREEN", ACTIVE_HOTKEY_MAC = "";
	
	/**
	 * Clipboard hotkeys
	 */
	public static final String CLIPBOARD_HOTKEY = "ctrl PAGE_UP", CLIPBOARD_HOTKEY_MAC = "ctrl shift 3";
	
	/**
	 * File upload hotkeys
	 */
	public static final String FILE_HOTKEY = "ctrl PAGE_DOWN", FILE_HOTKEY_MAC = "ctrl shift 4";
	
	/**
	 * Settings gui hotkeys
	 */
	public static final String OPTIONS_HOTKEY = "ctrl alt PRINTSCREEN", OPTIONS_HOTKEY_MAC = "ctrl shift 9";

	/**
	 * The hotkey provider
	 */
	private HotkeyProvider provider;

	/**
	 * The screensnapper instance
	 */
	private ScreenSnapper snapper;

	/**
	 * Whether the input has been initialized or disabled
	 */
	private boolean initialized = false;

	/**
	 * Construct a new hotkey manager
	 * 
	 * @param snapper
	 *            The screensnapper instance
	 */
	public HotkeyManager(ScreenSnapper snapper) {
		this.snapper = snapper;
		this.provider = HotkeyProvider.getCurrentProvider(false);
		// Register a shutdown hook just in case
		Runtime.getRuntime().addShutdownHook(new Thread() {
			public void run() {
				cleanupInput();
			}
		});
	}

	/**
	 * Initialize the input using JKeymaster
	 * @throws JSONException 
	 */
	public void initializeInput() {
		JSONObject keys = snapper.getConfiguration().getJSONObject("hotkeys");
		//A little sanity check, just in case we don't have ANY keys set
		if(keys == null) {
			return;
		}
		if (keys.has("crop")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("crop")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							snapper.crop();
						}
					});
		}
		if (keys.has("full")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("full")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							snapper.full();
						}
					});
		}
		if (keys.has("clipboard")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("clipboard")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							snapper.clipboard();
						}
					});
		}
		if (keys.has("file")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("file")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							snapper.selectFile();
						}
					});
		}
		if (keys.has("options")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("options")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							if (!snapper.openSettings()) {
								snapper.getTrayIcon()
										.displayMessage(
												"Error",
												"Could not open settings, is there another window open?",
												TrayIcon.MessageType.ERROR);
							}
						}
					});
		}
		// We support active windows only on windows/linux, but OSX SOON!
		if ((Platform.isWindows() || Platform.isLinux())
				&& keys.has("active")) {
			provider.register(KeyStroke.getKeyStroke(keys.getString("active")),
					new HotKeyListener() {
						@Override
						public void onHotKey(HotKey hotKey) {
							snapper.active();
						}
					});
		}
		initialized = true;
	}

	/**
	 * Clean up the input bindings
	 */
	public void cleanupInput() {
		resetKeys();
		provider.stop();
	}

	/**
	 * Reset the bound keys and let other classes know that they are not set
	 */
	public void resetKeys() {
		provider.reset();
		initialized = false;
	}

	/**
	 * Get whether this class has been initialized/keys have been bound
	 * 
	 * @return The value of <code>initialized</code>
	 */
	public boolean hasKeysBound() {
		return initialized;
	}
} public class Language {
	
	/**
	 * Pattern to match variables with
	 */
	private static final Pattern VAR_PATTERN = Pattern.compile("\\{(\\d+)\\}");
	
	/**
	 * Tokens in the current loaded language set
	 */
	private static Map<String, String> tokens = new HashMap<String, String>();
	
	/**
	 * Load a language after loading the default 'english' language to prevent missing phrases.
	 * @param lang
	 * 			The language name to load
	 * @throws IOException
	 * 			If an error occurs while loading
	 */
	public static void load(String lang) throws IOException {
		tokens.clear();
		
		loadAndMerge("english");
		if(!lang.equals("english")) {
			loadAndMerge(lang);
		}
	}
	
	/**
	 * Load and merge the tokens from a language
	 * @param lang
	 * 			The language to load
	 * @throws IOException
	 * 			If an error occurs, or if it's an invalid language file
	 */
	@SuppressWarnings("unchecked")
	public static void loadAndMerge(String lang) throws IOException {
		URL url = Util.getResourceByName("/languages/" + lang + ".json");
		if(url != null) {
			InputStream input = url.openStream();
			try {
				JSONObject obj = new JSONObject(new JSONTokener(input));
				
				JSONObject tokenObj = obj.getJSONObject("tokens");
				
				for(String s : (Set<String>) tokenObj.keySet()) {
					tokens.put(s, tokenObj.getString(s));
				}
			} finally {
				input.close();
			}
		} else {
			throw new IOException("Invalid language " + lang);
		}
	}

	/**
	 * Get a language string
	 * @param token
	 * 			The language token name
	 * @param args
	 * 			The language arguments
	 * @return
	 * 			The parsed language string, or "" if null
	 */
	public static String getString(String token, Object... args) {
		String str = tokens.get(token);
		
		if(str == null) {
			return "";
		}
		
		if(str.indexOf('{') != -1) {
			Matcher m = VAR_PATTERN.matcher(str);
			
			StringBuffer out = new StringBuffer();
			while(m.find()) {
				int paramIndex = Integer.parseInt(m.group(1));
				if(args.length > paramIndex-1) {
					m.appendReplacement(out, args[paramIndex-1].toString());
				}
			}
			m.appendTail(out);
			
			str = out.toString();
		}
		return str;
	}
} public class LoggingManager {
	
	/**
	 * Load the configuration from a byte array
	 * @throws IOException 
	 */
	@SuppressWarnings("unchecked")
	public static void configure() throws IOException {
		LoggerConfiguration config = new LoggerConfiguration();
		config.addHandlers(ConsoleHandler.class, FileLogHandler.class, LogPanelHandler.class);
		config.setLevel(Level.INFO);
		config.apply();
	}
} public class Updater {
	
	private static final Logger logger = Logger.getLogger(Updater.class.getName());
	
	/**
	 * The binary directory to use.
	 */
	private File binDirectory;
	
	/**
	 * Construct a new updater
	 */
	public Updater() {
		binDirectory = new File(Util.getWorkingDirectory(), "bin");
		binDirectory.mkdirs();
	}

	/**
	 * Check for updates
	 */
	public boolean checkUpdate(UpdaterReleaseType type, boolean prompt) {
		//Check for an update.
		try {
			logger.info("Checking for updates...");
			
			String data = HttpUtil.executeGet(Application.UPDATE_URL + type.getFeedPath());
			
			JSONObject obj = new JSONObject(data);
			
			//Compare versions
			String ver = obj.getString("version");
			
			String[] s = ver.split("\\.");
			int major = Integer.parseInt(s[0]), minor = Integer.parseInt(s[1]), patch = Integer.parseInt(s[2]);
			if(major > Version.MAJOR || major == Version.MAJOR && minor > Version.MINOR || major == Version.MAJOR && minor == Version.MINOR && patch > Version.PATCH) {
				logger.info("A new version is available. Current version: " + Version.getVersionString() + ", new version: " + major + "." + minor + "." + patch);
				if(prompt) {
					StringBuilder message = new StringBuilder();
					message.append("There is a new version of ").append(Application.NAME).append(" available!").append("\n");
					message.append("Your version: ").append(Version.getVersionString()).append("\n");
					message.append("Latest version: ").append(ver).append("\n");
					message.append("Click OK to download it, or Cancel to be prompted the next time you start ").append(Application.NAME);
					
					int choice = JOptionPane.showOptionDialog(null, message, "Update Available", JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE, null, new String[] {"OK", "Cancel"}, "OK");
					if(choice == 0) {
						logger.info("User confirmed the update.");
						
						applyUpdate(ver, new URL(obj.getString("file")));
						
						return true;
					} else {
						logger.info("User declined the update.");
					}
				} else {
					logger.info("Automatically applying update...");
					
					applyUpdate(ver, new URL(obj.getString("file")));
					
					return true;
				}
			} else {
				logger.info("No updates available.");
			}
		} catch (JSONException e) {
			logger.severe("Unable to check for update due to web service error.");
		} catch (IOException e) {
			//Unable to update
			e.printStackTrace();
		}
		return false;
	}
	
	/**
	 * Apply an update from the specified URL
	 * 
	 * @param version
	 * 			The version which we are downloading
	 * @param url
	 * 			The URL to download from
	 * @throws IOException
	 * 			If an error occurred while downloading
	 */
	public void applyUpdate(String version, URL url) throws IOException {
		// Construct the new path
		File out = new File(binDirectory, "Sleeksnap-v" + version + ".jar");
		
		logger.info("Creating new file...");
		// Create the new file
		out.createNewFile();
		
		logger.info("Downloading " + url + "...");
		// Download the new file
		download(url, out);
	}
	
	/**
	 * Download the specified file with a ProgressPanel to show progress.
	 * @param url
	 * 			The URL to download from
	 * @param file
	 * 			The file to download to
	 * @throws IOException
	 * 			If a problem occurred while starting the download
	 */
	public void download(URL url, final File file) throws IOException {
		JFrame frame = new JFrame("Sleeksnap Update");
		ProgressPanel panel = new ProgressPanel();
		frame.add(panel);
		frame.pack();
		frame.setResizable(false);
		frame.setVisible(true);
		frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
		
		try {
			frame.setIconImage(ImageIO.read(Util
					.getResourceByName("/icon32x32.png")));
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		
		Util.centerFrameOnMainDisplay(frame);
		
		Downloader downloader = new Downloader(url, new FileOutputStream(file));
		downloader.addListener(panel);
		downloader.addListener(new DownloadAdapter() {
			public void downloadFinished(Downloader downloader) {
				updateFinished(file);
			}
		});
		downloader.start();
	}
	
	/**
	 * Called when the update is finished to re-launch Sleeksnap
	 * 
	 * @param file	The newly downloaded jar file
	 */
	public void updateFinished(File file) {
		logger.info("Checking autostart...");
		verifyAutostart(file, VerificationMode.VERIFY);
		
		if(Platform.isWindows()) {
			logger.info("Checking start menu icon...");
			try {
				WindowsUpdater.checkStartMenu(file);
			} catch (Exception e) {
				logger.log(Level.WARNING, "Unable to update start menu icon", e);
			}
		}
		
		logger.info("Launching new file...");
		try {
			Launcher.launch(file, Launcher.class.getName());
			
			System.exit(0);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * If on windows or linux, verify that the registry entry is intact and pointing to the correct version.
	 * @param file
	 * 			The file to verify against
	 * @param mode
	 * 			The mode to use, REMOVE will remove the entry, VERIFY will update it, and INSERT will insert if it does not exist.
	 */
	public static void verifyAutostart(File file, VerificationMode mode) {
		if(Platform.isWindows()) {
			try {
				String current = WinRegistry.readString(WinRegistry.HKEY_CURRENT_USER, WinRegistry.RUN_PATH, Application.NAME);
				if(mode == VerificationMode.REMOVE) {
					WinRegistry.deleteValue(WinRegistry.HKEY_CURRENT_USER, WinRegistry.RUN_PATH, Application.NAME);
				} else if(mode == VerificationMode.INSERT && current == null || current != null && !current.equals(file.getAbsolutePath())) {
					WinRegistry.writeStringValue(WinRegistry.HKEY_CURRENT_USER, WinRegistry.RUN_PATH, Application.NAME, file.getAbsolutePath());
				}
			} catch (Exception e) {
				//Ignore it.
			}
		} else if(Platform.isX11()) {
			File autostartDir = new File(System.getenv("XDG_CONFIG_HOME") != null ? System.getenv("XDG_CONFIG_HOME") : System.getProperty("user.home") + "/.config/autostart");
			if(autostartDir.exists()) {
				File autostart = new File(autostartDir, Application.NAME.toLowerCase() + ".desktop");
				
				if(mode == VerificationMode.REMOVE) {
					autostart.delete();
				} else {
					String contents = new DesktopEntryBuilder()
						.addEntry("Type", "Application")
						.addEntry("Categories", "Accessories")
						.addEntry("Name", Application.NAME)
						.addEntry("Comment", "Sleeksnap - Java Screenshot Program")
						.addEntry("Terminal", false)
						.addEntry("Exec", FileUtils.getJavaExecutable().getAbsoluteFile() + " -jar \"" + file.getAbsolutePath() + "\"")
					.build();
					
					try {
						FileWriter writer = new FileWriter(autostart);
						try {
							writer.write(contents);
						} finally {
							writer.close();
						}
					} catch(IOException e) {
					}
				}
			}
		}
	}
	
	public enum VerificationMode {
		INSERT, VERIFY, REMOVE
	}
} public enum VerificationMode {
	INSERT, VERIFY, REMOVE
} public enum UpdaterMode {
	AUTOMATIC, PROMPT, MANUAL
} public enum UpdaterReleaseType {
	RECOMMENDED("latest.json"), DEVELOPMENT("latest_development.json");
	
	private String feedPath;

	private UpdaterReleaseType(String feedPath) {
		this.feedPath = feedPath;
	}
	
	public String getFeedPath() {
		return feedPath;
	}
} public class FileUpload implements Upload {

	/**
	 * The file in this upload
	 */
	private File file;
	
	public FileUpload(File file) {
		this.file = file;
	}
	
	/**
	 * Set the upload's file
	 * @param file
	 * 			The file to set
	 */
	public void setFile(File file) {
		this.file = file;
	}
	
	/**
	 * Get the file for the upload
	 * @return
	 * 		The file
	 */
	public File getFile() {
		return file;
	}
	
	/**
	 * Get this file as a FileInputStream
	 */
	@Override
	public InputStream asInputStream() {
		try {
			return new FileInputStream(file);
		} catch (FileNotFoundException e) {
			return null;
		}
	}
} public class ImageUpload implements Upload {
	
	/**
	 * The BufferedImage we are uploading
	 */
	private BufferedImage image;
	
	public ImageUpload(BufferedImage image) {
		this.image = image;
	}

	@Override
	public InputStream asInputStream() throws IOException {
		return ImageUtil.toInputStream(image);
	}
	
	/**
	 * Covert this image into a Base64 string
	 * @return
	 * 			The image in base64
	 * @throws IOException
	 * 			If an error occurred while writing/reading into base64
	 */
	public String toBase64() throws IOException {
		return ImageUtil.toBase64(image);
	}

	/**
	 * Set this upload's image
	 * @param image
	 * 			The image to set
	 */
	public void setImage(BufferedImage image) {
		this.image = image;
	}

	/**
	 * Get this upload's image
	 * @return
	 * 		The image of the upload
	 */
	public BufferedImage getImage() {
		return image;
	}
} public class TextUpload implements Upload {

	/**
	 * The upload text data
	 */
	private String text;
	
	public TextUpload(String text) {
		this.text = text;
	}
	
	@Override
	public InputStream asInputStream() {
		return new ByteArrayInputStream(text.getBytes());
	}
	
	/**
	 * Get this upload's contents
	 * 
	 * @return
	 * 			The upload contents
	 */
	public String getText() {
		return text;
	}
	
	@Override
	public String toString() {
		return text;
	}
} public class URLUpload implements Upload {
	
	/**
	 * The URL of this upload
	 */
	private URL url;
	
	public URLUpload(URL url) {
		this.url = url;
	}

	public URLUpload(String url) throws MalformedURLException {
		this.url = new URL(url);
	}

	@Override
	public InputStream asInputStream() throws IOException {
		throw new IOException("URLs cannot be transformed into InputStreams!");
	}
	
	/**
	 * Set the URL
	 * @param url
	 * 			The URL to set it to
	 */
	public void setURL(URL url) {
		this.url = url;
	}

	/**
	 * Get the URL
	 * @return
	 * 		The URL
	 */
	public URL getURL() {
		return url;
	}
	
	@Override
	public String toString() {
		return url.toString();
	}
} public interface Upload {

	/**
	 * Get this upload as an InputStream
	 * @return
	 * 			The InputStream containing the upload data
	 */
	public InputStream asInputStream() throws IOException;
} public abstract class Uploader<T extends Upload> {
	
	/**
	 * The parent uploader, used if this is a sub uploader of another generic uploader
	 */
	protected Uploader<?> parent;
	
	private Object settingsInstance;
	
	public Uploader() {
		
	}
	
	public Uploader(Uploader<?> parent) {
		this.parent = parent;
	}

	/**
	 * Get the uploader name
	 * 
	 * @return
	 * 		The uploader name
	 */
	public abstract String getName();

	/**
	 * Upload the specified object
	 * 
	 * @param t
	 *      The object
	 * @return
	 * 		The URL or Location
	 * @throws Exception
	 */
	public abstract String upload(T t) throws Exception;
	
	/**
	 * Can be overridden by the uploader to validate the settings.
	 * 
	 * If invalid, either throw an UploaderConfigurationException or display your own message.
	 * 
	 * @return
	 * 			true if valid, false if invalid.
	 */
	public boolean validateSettings() throws UploaderConfigurationException {
		return true;
	}
	
	/**
	 * Can be overidden to get a call when it is activated/set as default (On Load or User Settings)
	 * By default, if this is a sub uploader for a Generic uploader, it will call the parent's onActivation method
	 */
	public void onActivation() {
		if(parent != null) {
			parent.onActivation();
		}
	}
	
	/**
	 * Set this uploader's parent
	 */
	public void setParentUploader(Uploader<?> parent) {
		this.parent = parent;
	}
	
	public Uploader<?> getParentUploader() {
		return parent;
	}

	public boolean hasParent() {
		return parent != null;
	}

	/**
	 * Check if this class directly has settings
	 * @return
	 * 		True if this class has settings
	 */
	public boolean hasDirectSettings() {
		return getClass().isAnnotationPresent(SettingsClass.class);
	}

	/**
	 * Check if this uploader (or generic uploader parent) has settings
	 * @return
	 * 		True if this uploader has settings
	 */
	public boolean hasSettings() {
		return hasDirectSettings() || parent != null && parent.hasSettings();
	}

	/**
	 * Get the Settings annotation from an uploader
	 * 
	 * @return The settings, or null if it doesn't have any
	 */
	public SettingsClass getSettingsAnnotation() {
		SettingsClass settings = getClass().getAnnotation(SettingsClass.class);

		if (settings == null && parent != null) {
			settings = parent.getSettingsAnnotation();
		}
		return settings;
	}
	
	public void saveSettings(File file) throws IOException {
		Writer writer = new FileWriter(file);
		try {
			ScreenSnapper.GSON.toJson(settingsInstance, writer);
		} finally {
			writer.close();
		}
	}
	
	public Object getSettingsInstance() {
		return settingsInstance;
	}

	public void setSettingsInstance(Object settingsInstance) {
		this.settingsInstance = settingsInstance;
	}
} public class UploaderConfigurationException extends UploadException {

	private static final long serialVersionUID = 2574424530734464288L;

	public UploaderConfigurationException(String string) {
		super(string);
	}
} public class UploaderLoader {
	
	/**
	 * The Screenshot program instance
	 */
	private ScreenSnapper snapper;

	public UploaderLoader(ScreenSnapper snapper) {
		this.snapper = snapper;
	}

	/**
	 * Scan the uploader directory for either class files or jar files
	 */
	public void load() throws Exception {
		// Load custom uploaders
		File dir = new File(Util.getWorkingDirectory(), "plugins/uploaders");
		if (!dir.exists()) {
			dir.mkdirs();
		}
		
		ClassLoader fileLoader = new URLClassLoader(
				new URL[] { dir.toURI().toURL() });
		for (File f : dir.listFiles()) {
			String name = f.getName();
			if (name.endsWith(".class") && !name.contains("$")) {
				loadSingleClassUploader(fileLoader, f);
			} else if(name.endsWith(".jar")) {
				loadPackedUploader(f);
			}
		}
	}
	
	/**
	 * Load a single uploader class
	 * @param loader
	 * 			The class loader for the directory
	 * @param file
	 * 			The file to load
	 */
	@SuppressWarnings("unchecked")
	private void loadSingleClassUploader(ClassLoader loader, File file) {
		String name = file.getName().replaceAll(
				".class", "");
		try {
			Class<?> c = loader.loadClass(name);
			
			snapper.registerUploaderClass((Class<? extends Uploader<?>>) c);
		} catch (Exception e) {
			JOptionPane.showMessageDialog(null,
				"An exception occured when loading " + name + " : " + e + ", it could be outdated.",
				"Could not load uploader : " + name,
				JOptionPane.ERROR_MESSAGE);
		}
	}
	
	/**
	 * Load a packed uploader jar
	 * @param file
	 * 			The file to load
	 */
	@SuppressWarnings("unchecked")
	private void loadPackedUploader(File file) {
		String name = file.getName();
		try {
			JarFile jar = new JarFile(file);
			
			ClassLoader loader = new URLClassLoader(new URL[] { file.toURI().toURL() });
			
			Manifest manifest = jar.getManifest();
			
			if(manifest != null) {
				Attributes attrs = manifest.getMainAttributes();
				
				String uploaderClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
				
				if(uploaderClass == null)
					throw new Exception("Unable to find Main-Class attribute");
				
				// Attempt to load it
				
				Class<? extends Uploader<?>> c = (Class<? extends Uploader<?>>) loader.loadClass(uploaderClass);
				
				snapper.registerUploaderClass(c);
			} else {
				throw new Exception("Unable to find Manifest file");
			}
			
			jar.close();
		} catch (Exception e) {
			JOptionPane.showMessageDialog(null,
					"An exception occured when loading " + name + " : "
							+ e + ", it could be outdated.",
					"Could not load uploader : " + name,
					JOptionPane.ERROR_MESSAGE);
		}
	}
} @SettingsClass(FilebinUploader.FilebinSettings.class)
public class FilebinUploader extends Uploader<FileUpload> {
	
	/**
	 * The Filebin API URL
	 */
	private static final String API_URL = "http://filebin.ca/upload.php";
	
	private FilebinSettings settings;
	
	public FilebinUploader(FilebinSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Filebin.ca";
	}

	@Override
	public String upload(FileUpload file) throws Exception {
		MultipartPostMethod post = new MultipartPostMethod(API_URL);
		post.setParameter("file", file.getFile());
		if(settings.apikey != null && !settings.apikey.isEmpty()) {
			post.setParameter("key", settings.apikey);
		}
		post.execute();
		String resp = post.getResponse();
		//Parsing it is not needed, but it's a good idea to make it easy to use.
		Map<String, String> res = new HashMap<String, String>();
		//The format is simple key:value, with url being a key, status will be 'error' or 'fail' on an error.
		String[] lines = resp.split("\n");
		for(String s : lines) {
			int idx = s.indexOf(':');
			if(idx != -1) {
				res.put(s.substring(0, idx), s.substring(idx+1));
			}
		}
		if(!res.containsKey("url")) {
			throw new UploadException(res.get("status"));
		}
		return res.get("url");
	}

	public static class FilebinSettings {
		@Setting(name = "API Key", description = "File Upload API Key", optional = true)
		public String apikey;
	}
} public class UppitUploader extends Uploader<FileUpload> {

	private static Pattern urlPattern = Pattern.compile("action=\"(.*?)\"");
	private static Pattern paramPattern = Pattern
			.compile("<input type=\"hidden\" name=\"(.*?)\" value=\"(.*?)\">");
	private static Pattern finalPattern = Pattern
			.compile("<textarea .*?>(.*?)\\s*</textarea>");

	@Override
	public String getName() {
		return "Uppit.com";
	}

	@Override
	public String upload(FileUpload t) throws Exception {
		String contents = HttpUtil.executeGet("http://uppit.com/");
		Matcher matcher = urlPattern.matcher(contents);
		if (matcher.find()) {
			MultipartPostMethod method = new MultipartPostMethod(
					matcher.group(1));
			matcher = paramPattern.matcher(contents);
			while (matcher.find()) {
				method.setParameter(matcher.group(1), matcher.group(2));
			}
			method.setParameter("tos", "1");
			method.setParameter("file_1", MultipartFile.create(t.getFile()));

			method.execute();
			try {
				matcher = finalPattern.matcher(method.getResponse());

				if (matcher.find()) {
					return matcher.group(1);
				}
			} finally {
				method.close();
			}
			throw new Exception("Unable to find url!");
		}
		return null;
	}
} @SettingsClass(FTPUploader.FTPUploaderSettings.class)
public class FTPUploader extends GenericUploader {
	
	private Uploader<?>[] uploaders = new Uploader<?>[] { new FTPImageUploader(), new FTPTextUploader(), new FTPFileUploader() };

	/**
	 * The settings object used for this uploader
	 */
	private FTPUploaderSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public FTPUploader(FTPUploaderSettings settings) {
		this.settings = settings;
	}

	/**
	 * Upload a file to the FTP server
	 * 
	 * @param fileName
	 *            The filename
	 * @param input
	 *            The input stream
	 * @return The final URL
	 * @throws IOException
	 *             If an error occurred
	 */
	public String ftpUpload(String fileName, InputStream input)
			throws IOException, UploaderConfigurationException {
		if (settings.hostname == null || settings.hostname.isEmpty()
				|| settings.username == null || settings.username.isEmpty()
				|| settings.password == null // Password can be empty.
				|| settings.baseurl == null) {
			throw new UploaderConfigurationException(
					"Missing hostname, username, password or baseurl!");
		}
		SimpleFTP ftp = new SimpleFTP();
		
		try {
			ftp.connect(settings.hostname, settings.port, settings.username, settings.password.toString());
		} catch (IOException e) {
			e.printStackTrace();
			throw new UploaderConfigurationException("Unable to connect to FTP server, please check your username and password.");
		}
		
		if (settings.remotedir != null && !settings.remotedir.isEmpty()) {
			if(!ftp.cwd(settings.remotedir)) {
				throw new UploaderConfigurationException("Unable to change FTP directory.");
			}
		}
		
		try {
			ftp.stor(input, fileName);
		} finally {
			ftp.disconnect();
			input.close();
		}
		String baseUrl = settings.baseurl != null ? settings.baseurl : "%s";
		if(!baseUrl.contains("%s")) {
			baseUrl = baseUrl + "%s";
		}
		return String.format(baseUrl, fileName);
	}

	/**
	 * An uploader to deal with Image uploads
	 * 
	 * @author Nikki
	 * 
	 */
	public class FTPImageUploader extends Uploader<ImageUpload> {

		@Override
		public String getName() {
			return FTPUploader.this.getName();
		}

		@Override
		public String upload(ImageUpload t) throws Exception {
			return ftpUpload(generateFileName(t), t.asInputStream());
		}
	}

	/**
	 * An uploader to deal with Text uploads
	 * 
	 * @author Nikki
	 * 
	 */
	public class FTPTextUploader extends Uploader<TextUpload> {

		@Override
		public String getName() {
			return FTPUploader.this.getName();
		}

		@Override
		public String upload(TextUpload t) throws Exception {
			return ftpUpload(generateFileName(t), t.asInputStream());
		}
	}

	/**
	 * An uploader to deal with File uploads
	 * 
	 * @author Nikki
	 * 
	 */
	public class FTPFileUploader extends Uploader<FileUpload> {

		@Override
		public String getName() {
			return FTPUploader.this.getName();
		}

		@Override
		public String upload(FileUpload t) throws Exception {
			return ftpUpload(generateFileName(t), t.asInputStream());
		}
	}

	/**
	 * Generate a file name from a date object formatted for filenames, plus the
	 * filename if applicable
	 * 
	 * @param object
	 *            The object to be uploaded
	 * @return The filename
	 */
	public String generateFileName(Object object) {
		String name = DateUtil.getCurrentDate();
		if (object instanceof ImageUpload) {
			name += ".png";
		} else if (object instanceof TextUpload) {
			name += ".txt";
		} else if (object instanceof FileUpload) {
			name += ((File) object).getName();
		} else {
			name += ".file";
		}
		return name;
	}

	@Override
	public Uploader<?>[] getSubUploaders() {
		return uploaders;
	}

	@Override
	public String getName() {
		return "FTP Server";
	}

	public static class FTPUploaderSettings {
		
		@Setting(name = "Host", description = "FTP Server Host")
		public String hostname;

		@Setting(name = "Username", description = "FTP Server Username")
		public String username;

		@Setting(name = "Password", description = "FTP Server Password")
		public Password password;
		
		@Setting(name = "Base URL", description = "Webserver base url to use")
		public String baseurl;
		
		@Setting(name = "Port", description = "FTP Server Port", optional = true, defaults = { "min=1", "max=65535", "default=21" })
		public int port = 21;
		
		@Setting(name = "Remote Directory", description = "Remote Directory to upload to", optional = true)
		public String remotedir;
	}
} public abstract class GenericUploader extends Uploader<Upload> {

	/**
	 * Get the uploaders that this uploader can use
	 * 
	 * @return The uploaders
	 */
	public abstract Uploader<?>[] getSubUploaders();

	@Override
	public String upload(Upload t) throws Exception {
		return null;
	}
} public class LocalFileUploader extends GenericUploader {
	
	/**
	 * Local base directory
	 */
	private static final File LOCAL_BASE_DIR = new File(Util.getWorkingDirectory(), "local");
	
	/**
	 * Local image directory
	 */
	private static final File LOCAL_IMAGE_DIR = new File(LOCAL_BASE_DIR, "images");
	
	/**
	 * Local text directory
	 */
	private static final File LOCAL_TEXT_DIR = new File(LOCAL_BASE_DIR, "text");
	
	/**
	 * List of sub uploaders
	 */
	private final Uploader<?>[] uploaders = new Uploader<?>[] { new ImageLocalFileUploader(), new TextLocalFileUploader() };

	/**
	 * An ImageUploader using ImageIO.write(image, format, file) to save an image
	 * 
	 * @author Nikki
	 *
	 */
	public class ImageLocalFileUploader extends Uploader<ImageUpload> {

		@Override
		public String getName() {
			return LocalFileUploader.this.getName();
		}

		@Override
		public String upload(ImageUpload upload) throws Exception {
			checkDirectory(upload);
			File file = new File(LOCAL_IMAGE_DIR, FileUtils.generateFileName(upload));
			ImageIO.write(upload.getImage(), "png", file);
			return file.toURI().toURL().toString();
		}
	}

	/**
	 * A text file uploader using FileWriter to save text files to disk
	 * 
	 * @author Nikki
	 */
	public class TextLocalFileUploader extends Uploader<TextUpload> {

		@Override
		public String getName() {
			return LocalFileUploader.this.getName();
		}

		@Override
		public String upload(TextUpload upload) throws Exception {
			checkDirectory(upload);
			File file = new File(LOCAL_TEXT_DIR, FileUtils.generateFileName(upload));
			FileWriter writer = new FileWriter(file);
			try {
				writer.write(upload.getText());
			} finally {
				writer.close();
			}
			return file.toURI().toURL().toString();
		}
	}
	
	/**
	 * Check and create any directories needed
	 * @param upload
	 * 			The upload to check for the type
	 */
	protected void checkDirectory(Upload upload) {
		if(!LOCAL_BASE_DIR.exists()) {
			LOCAL_BASE_DIR.mkdirs();
		}
		if(upload instanceof ImageUpload && !LOCAL_IMAGE_DIR.exists()) {
			LOCAL_IMAGE_DIR.mkdirs();
		} else if (upload instanceof TextUpload && !LOCAL_TEXT_DIR.exists()) {
			LOCAL_TEXT_DIR.mkdirs();
		}
	}

	@Override
	public Uploader<?>[] getSubUploaders() {
		return uploaders;
	}

	@Override
	public String getName() {
		return "Local File";
	}

} public class ImageLocalFileUploader extends Uploader<ImageUpload> {

	@Override
	public String getName() {
		return LocalFileUploader.this.getName();
	}

	@Override
	public String upload(ImageUpload upload) throws Exception {
		checkDirectory(upload);
		File file = new File(LOCAL_IMAGE_DIR, FileUtils.generateFileName(upload));
		ImageIO.write(upload.getImage(), "png", file);
		return file.toURI().toURL().toString();
	}
} @SettingsClass(ImageScriptUploader.ImageScriptSettings.class)
public class ImageScriptUploader extends Uploader<ImageUpload> {

	/**
	 * The image hosting script settings.
	 */
	private final ImageScriptSettings settings;

	/**
	 * Construct a new ImageScriptUploader.
	 *
	 * @param settings The settings instance.
	 */
	public ImageScriptUploader(ImageScriptSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Image Hosting Script";
	}

	@Override
	public String upload(ImageUpload imageUpload) throws Exception {
		switch (settings.formType.toLowerCase()) {
			case "base64":
				String resp = HttpUtil.executePost(settings.url, settings.variableName + "=" + URLEncoder.encode(imageUpload.toBase64(), "UTF-8"));
				if(!resp.substring(0, 4).equals("http")) {
					throw new UploadException(resp);
				}
				return resp;
			case "multipart":
				MultipartPostMethod method = new MultipartPostMethod(settings.url);
				method.setParameter(settings.variableName, new MultipartPostMethod.MultipartFile(Utils.FileUtils.generateFileName(imageUpload), imageUpload.asInputStream()));
				method.execute();
				return method.getResponse();
			default:
				throw new UploadException("Unknown form type.");
		}
	}

	public static class ImageScriptSettings {

		@Setting(name = "Script URL", description = "Path to image upload script (including url)")
		public String url;

		@Setting(name = "Form Variable", description = "Form variable name")
		public String variableName;

		@Setting(name = "Form Type", description = "Form Type, Normal (Base64 Encoded) or Multipart.", type = ComboBoxSettingType.class, defaults = { "Base64", "Multipart" })
		public String formType = "Base64";

	}
} public class ImagebinUploader extends Uploader<ImageUpload> {

	private static final String UPLOAD_URL = "http://imagebin.ca/upload.php";
	
	@Override
	public String upload(ImageUpload image) throws Exception {
		MultipartPostMethod m = new MultipartPostMethod(UPLOAD_URL);
		m.setParameter("file", new MultipartFile("Sleeksnap-" + DateUtil.getCurrentDate()
				+ ".png", image.asInputStream()));
		m.execute();
		String resp = m.getResponse();
		//Parsing it is not needed, but it's a good idea to make it easy to use.
		Map<String, String> res = new HashMap<String, String>();
		//The format is simple key:value, with url being a key, status will be 'error' or 'fail' on an error.
		String[] lines = resp.split("\n");
		for(String s : lines) {
			int idx = s.indexOf(':');
			if(idx != -1) {
				res.put(s.substring(0, idx), s.substring(idx+1));
			}
		}
		if(!res.containsKey("url")) {
			throw new UploadException(res.get("status"));
		}
		return res.get("url");
	}

	@Override
	public String getName() {
		return "Imagebin";
	}

} @SettingsClass(ImgurUploader.ImgurSettings.class)
public class ImgurUploader extends Uploader<ImageUpload> {

	public static final String CLIENT_ID = APICredentials.getCredential(ImgurUploader.class, "clientId");
	public static final String CLIENT_SECRET = APICredentials.getCredential(ImgurUploader.class, "clientSecret");

	private ImgurAuthentication auth = new ImgurAuthentication(this);

	/**
	 * The settings object used for this uploader
	 */
	private ImgurSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public ImgurUploader(ImgurSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Imgur";
	}

	@Override
	public String upload(ImageUpload image) throws Exception {
		// The API URL
		URL url = new URL("https://api.imgur.com/3/image.json");

		// Encode the image using our utility class
		RequestData req = new RequestData();
		req.put("image", ImageUtil.toBase64(image.getImage()));

		// Open a connection to the API and add our Client ID
		HttpURLConnection connection = (HttpURLConnection) url.openConnection();
		connection.setRequestMethod("POST");
		auth.addToConnection(connection);
		connection.setDoOutput(true);

		try {
			/**
			 * Write the image data and api key
			 */
			OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
			writer.write(req.toURLEncodedString());
			writer.flush();
			writer.close();
	
			String res = StreamUtils.readContents(connection.getInputStream());
			
			if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
				try {
	
					/**
					 * Parse the URL from the response
					 */
					JSONObject object = new JSONObject(res);
	
					JSONObject data = object.getJSONObject("data");
	
					if (!object.getBoolean("success")) {
						JSONObject error = data.getJSONObject("error");
						throw new UploadException(error.getString("message"));
					}
	
					return data.getString("link");
				} catch (JSONException e) {
					throw new UploadException("Malformed JSON Response");
				}
			} else {
				throw new UploadException("Imgur API returned HTTP Response " + connection.getResponseCode());
			}
		} finally {
			connection.disconnect();
		}
	}

	public ImgurSettings getSettings() {
		return settings;
	}
	
	public static class ImgurSettings {
		@Setting(name = "Account", description = "Imgur Account", type = ImgurOAuthSettingType.class)
		public ImgurAuthenticationObject account;
	}
} public class ImmioUploader extends Uploader<ImageUpload> {
	
	private static final String API_URL = "http://imm.io/store/";

	@Override
	public String getName() {
		return "Imm.io";
	}

	@Override
	public String upload(ImageUpload image) throws Exception {
		MultipartPostMethod post = new MultipartPostMethod(API_URL);
		post.setParameter("image", new MultipartFile(FileUtils.generateFileName("png"), image.asInputStream()));
		post.setParameter("meta", new JSONObject().put("referer", new JSONObject().put("name", Application.NAME).put("url", Application.URL)).toString());
		post.execute();
		//Read the response as JSON
		JSONObject object = new JSONObject(post.getResponse());
		if(object.getBoolean("success")) {
			JSONObject payload = object.getJSONObject("payload");
			return payload.getString("uri");
		} else {
			throw new UploadException(object.getString("payload"));
		}
	}
} @SettingsClass(PuushUploader.PuushSettings.class)
public class PuushUploader extends Uploader<ImageUpload> {

	/**
	 * The auth page URL
	 */
	private static final String API_AUTH_URL = "http://puush.me/api/auth";
	
	/**
	 * The upload URL
	 */
	private static final String API_UPLOAD_URL = "http://puush.me/api/up";

	/**
	 * The settings object used for this uploader
	 */
	private PuushSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public PuushUploader(PuushSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Puu.sh";
	}

	@Override
	public String upload(ImageUpload image) throws Exception {
		if (settings.apikey == null || settings.apikey.isEmpty()) {
			throw new UploaderConfigurationException("API Key is not set! Please configure this uploader's settings.");
		}

		MultipartPostMethod post = new MultipartPostMethod(API_UPLOAD_URL);

		post.setParameter("k", settings.apikey);

		post.setParameter("z", "sleeksnap");

		post.setParameter("f",
				new MultipartFile("Sleeksnap-" + DateUtil.getCurrentDate()
						+ ".png", image.asInputStream()));

		post.execute();

		String[] fields = post.getResponse().split(",");

		if (fields.length == 1) {
			throw new UploadException("Puu.sh returned the wrong data!");
		}
		return fields[1];
	}
	
	@Override
	public boolean validateSettings() throws UploaderConfigurationException {
		if (settings.email == null || settings.email.isEmpty() || settings.password == null || settings.password.isEmpty()) {
			throw new UploaderConfigurationException("Username or password not set, please reconfigure puush's uploader!");
		}
		try {
			RequestData data = new RequestData();
			
			data.put("e", settings.email);
			data.put("p", settings.password);
			
			String resp = HttpUtil.executePost(API_AUTH_URL, data).trim();
			if(resp.startsWith("-1")) {
				throw new UploaderConfigurationException("Invalid login, please try again.");
			}
			
			String[] s = resp.split(",");
			
			if(s[1].length() > 0) {
				settings.apikey = s[1];
			}
		} catch (IOException e) {
			throw new UploaderConfigurationException("Unable to validate auth due to unexpected error");
		} finally {
			settings.password = null;
		}
		return true;
	}
	
	public static class PuushSettings {
		@Setting(name = "E-mail", description = "Puush Login E-mail")
		public String email;
		
		@Setting(name = "Password", description = "Puush Login Password", type = PasswordSettingType.class)
		public String password;
		
		
		public String apikey;
	}
} public class LaravelUploader extends Uploader<TextUpload> {

	private static final String LARAVEL_URL = "http://laravel.io/bin";

	@Override
	public String upload(TextUpload t) throws Exception {
		RequestData data = new RequestData();
		data.put("code", t.getText());
		return HttpUtil.executePost(LARAVEL_URL, data, ResponseType.REDIRECT_URL);
	}

	@Override
	public String getName() {
		return "Laravel";
	}

} public class Paste2Uploader extends Uploader<TextUpload> {
	
	private static final String APIURL = "http://paste2.org/new-paste";

	@Override
	public String upload(TextUpload t) throws Exception {
		RequestData data = new RequestData();
		
		data.put("code", t.getText())
			.put("description", "")
			.put("lang", "text")
			.put("parent", "");
		
		return HttpUtil.executePost(APIURL, data, ResponseType.REDIRECT_URL);
	}

	@Override
	public String getName() {
		return "Paste2";
	}
} @SettingsClass(PastebinUploader.PastebinSettings.class)
public class PastebinUploader extends Uploader<TextUpload> {

	/**
	 * Sleeksnap's API key
	 */
	private static final String API_KEY = APICredentials.getCredential(PastebinUploader.class, "key");

	/**
	 * API Options
	 */
	private static final String API_OPTION_PASTE = "paste";

	/**
	 * The URL of the API page
	 */
	private static final String API_URL = "http://pastebin.com/api/api_post.php";
	
	/**
	 * The URL of the API Auth page
	 */
	private static final String API_AUTH_URL = "http://pastebin.com/api/api_login.php";

	/**
	 * The settings object used for this uploader
	 */
	private PastebinSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public PastebinUploader(PastebinSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Pastebin.com";
	}

	@Override
	public String upload(TextUpload contents) throws Exception {
		RequestData data = new RequestData();
		
		data.put("api_dev_key", API_KEY)
			.put("api_option", API_OPTION_PASTE)
			.put("api_paste_code", contents.getText());
		
		// User signed in through API
		if (settings.apikey != null && !settings.apikey.isEmpty()) {
			data.put("api_user_key", settings.apikey);
		}
		
		// Paste exposure is set.
		if (settings.paste_exposure != null) {
			if (settings.paste_exposure == PastebinExposure.Private
					&& (settings.apikey == null || settings.apikey.isEmpty())) {
				throw new UploaderConfigurationException(
						"Pastebin.com only supports private pastes while logged in!");
			} else {
				data.put("api_paste_private", settings.paste_exposure.ordinal());
			}
		}
		
		// Expiration, this'll probably be set.
		if (settings.expiration != null) {
			String exp = settings.expiration;
			
			if(exp.indexOf(' ') != -1) {
				exp = exp.substring(0, exp.indexOf(' ')) + Character.toUpperCase(exp.charAt(exp.indexOf(' ')+1));
			}
			
			if (!exp.equals("Never")) {
				data.put("api_paste_expire_date", exp);
			}
		}
		
		// Execute it and let the user know if something is wrong with it.
		String resp = HttpUtil.executePost(API_URL, data);
		
		if (resp.startsWith("Bad")) {
			throw new UploadException(resp.substring(resp.indexOf(',') + 2));
		}
		
		return resp;
	}

	@Override
	public boolean validateSettings() throws UploaderConfigurationException {
		if (settings.username != null && settings.password != null && !settings.username.isEmpty() && !settings.password.isEmpty()) {
			// Validate the username and password, then get us a key.
			RequestData data = new RequestData();
			data.put("api_dev_key", API_KEY)
				.put("api_user_name", settings.username)
				.put("api_user_password", settings.password);
			try {
				String resp = HttpUtil.executePost(API_AUTH_URL, data);
				if (resp.startsWith("Bad")) {
					throw new UploaderConfigurationException(
							resp.substring(resp.indexOf(',') + 2));
				} else {
					settings.apikey = resp;
				}
			} catch (IOException e) {
				throw new UploaderConfigurationException(
						"Unable to verify username and password");
			}
		}
		// We don't need to store the user's password since we get an application key
		settings.password = null;
		return true;
	}
	
	public static class PastebinSettings {
		// "username", "password|password", "paste_exposure|combobox[Public,Unlisted,Private]", "expiration|combobox[Never,10 minutes,1 hour,1 day,1 week,2 weeks,1 month]"
		@Setting(name = "Username", description = "Pastebin.com Account Username", optional = true)
		public String username;
		
		@Setting(name = "Password", description = "Pastebin.com Account Password", type = PasswordSettingType.class, optional = true)
		public String password;
		
		@Setting(name = "Paste Exposure", description = "Pastebin Paste Exposure")
		public PastebinExposure paste_exposure = PastebinExposure.Public;
		
		@Setting(name = "Expiration", description = "Paste Expiration Time", type = ComboBoxSettingType.class, defaults = { "Never", "10 minutes", "1 hour", "1 day", "1 week", "2 weeks", "1 month" })
		public String expiration;
		
		// The user API Key, this is automatically generated so it doesn't need a Setting annotation.
		public String apikey;
	}

	/**
	 * Pastebin exposure settings.
	 * 
	 * Documented as: We have 3 valid values available which you can use with
	 * the 'api_paste_private' parameter: 0 = Public 1 = Unlisted 2 = Private
	 * (only allowed in combination with api_user_key, as you have to be logged
	 * into your account to access the paste)
	 * 
	 * @author Nikki
	 * 
	 */
	private enum PastebinExposure {
		Public, Unlisted, Private
	}
} @SettingsClass(PastebincaUploader.PastebincaSettings.class)
public class PastebincaUploader extends Uploader<TextUpload> {

	/**
	 * Basic variables, such as the API Key and URL
	 */
	private static final String PASTEBINCA_URL = "http://pastebin.ca/";
	private static final String PASTEBINCA_SCRIPTURL = PASTEBINCA_URL
			+ "quiet-paste.php";
	
	private static final String PASTEBINCA_KEY = APICredentials.getCredential(PastebincaUploader.class, "key");
	
	/**
	 * The settings object used for this uploader
	 */
	private PastebincaSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public PastebincaUploader(PastebincaSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Pastebin.ca";
	}

	@Override
	public String upload(TextUpload text) throws Exception {		
		RequestData data = new RequestData();
		
		data.put("api", PASTEBINCA_KEY)
			.put("content", text.getText())
			.put("s", true)
			.put("type", "1")
			.put("expiry", settings.expiration)
			.put("name", "");
		
		String resp = HttpUtil.executePost(PASTEBINCA_SCRIPTURL, data);

		return PASTEBINCA_URL + resp.substring(resp.indexOf(':') + 1);
	}
	
	public static class PastebincaSettings {
		
		@Setting(name = "Paste Expiration", description = "Time until paste expires", type = ComboBoxSettingType.class, defaults = { "Never", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "45 minutes", "1 hour", "2 hours", "4 hours", "8 hours", "12 hours", "1 day", "2 days", "3 days", "1 week", "2 weeks", "3 weeks", "1 month", "2 months", "3 months", "4 months", "5 months", "6 months", "1 year" })
		public String expiration = "Never";
	
	}
} @SettingsClass(PasteeUploader.PasteeSettings.class)
public class PasteeUploader extends Uploader<TextUpload> {
	
	/**
	 * The settings object used for this uploader
	 */
	private PasteeSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public PasteeUploader(PasteeSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Paste.ee";
	}

	@Override
	public String upload(TextUpload upload) throws Exception {
		RequestData data = new RequestData();
		
		data.put("key", settings.apiKey != null && !settings.apiKey.isEmpty() ? settings.apiKey : "public")
			.put("language", "plain")
			.put("format", "simple")
			.put("paste", upload.getText());
		
		if(settings.expireViews > 0) {
			data.put("expire", "views;" + settings.expireViews);
		} else {
			data.put("expire", FormatUtil.formattedTimeToMinutes(settings.expiration != null ? settings.expiration.toString() : PasteeExpiration.NO_EXPIRATION.toString()));
		}
		
		return HttpUtil.executePost("http://paste.ee/api", data);
	}
	
	public static class PasteeSettings {
		@Setting(name = "API Key", description = "Paste.ee API Key", defaults = "public", optional = true)
		public String apiKey = "public";

		@Setting(name = "Description", description = "Paste Description", defaults = "", optional = true)
		public String description = "";

		@Setting(name = "Expiration", description = "Paste Expiration Time", optional = true)
		public PasteeExpiration expiration = PasteeExpiration.NO_EXPIRATION;

		@Setting(name = "Expiration Views", description = "Paste Expiration Views", defaults = "0", optional = true)
		public int expireViews;
		
		public enum PasteeExpiration {
			NO_EXPIRATION(0, TimeUnit.SECONDS),
			FIVE_MINUTES(5, TimeUnit.MINUTES),
			FIFTEEN_MINUTES(15, TimeUnit.MINUTES),
			THIRTY_MINUTES(30, TimeUnit.MINUTES),
			ONE_HOUR(1, TimeUnit.HOURS),
			SIX_HOURS(6, TimeUnit.HOURS),
			TWELVE_HOURS(12, TimeUnit.HOURS),
			ONE_DAY(1, TimeUnit.DAYS),
			THREE_DAYS(3, TimeUnit.DAYS),
			FIVE_DAYS(5, TimeUnit.DAYS),
			TEN_DAYS(10, TimeUnit.DAYS),
			FIFTEEN_DAYS(15, TimeUnit.DAYS),
			ONE_MONTH(30, TimeUnit.DAYS);
			
			/**
			 * The time in the specified unit for this expiration
			 */
			private int time;
			
			/**
			 * The time unit for this expiration
			 */
			private TimeUnit unit;

			private PasteeExpiration(int time, TimeUnit unit) {
				this.time = time;
				this.unit = unit;
			}
			
			/**
			 * Convert this expiration time to minutes
			 * @return
			 * 		The converted time in minutes
			 */
			public int toMinutes() {
				return (int) unit.toMinutes(time);
			}
			
			/**
			 * Convert this value to a string. This is necessary because we cannot use numbers.
			 */
			@Override
			public String toString() {
				String name = unit.name().toLowerCase();
				if (time == 1) {
					name = name.substring(0, name.length()-1);
				}
				return time == 0 ? "No Expiration" : time + " " + name;
			}
		}
	}
} @SettingsClass(PastieUploader.PastieSettings.class)
public class PastieUploader extends Uploader<TextUpload> {

	/**
	 * The submission url
	 */
	private static final String PASTIE_URL = "http://pastie.org/pastes";
	
	/**
	 * The settings object used for this uploader
	 */
	private PastieSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public PastieUploader(PastieSettings settings) {
		this.settings = settings;
	}

	@Override
	public String getName() {
		return "Pastie.org";
	}

	@Override
	public String upload(TextUpload t) throws Exception {
		RequestData data = new RequestData();
		
		data.put("paste[parser]", "plain_text")
			.put("paste[body]", t.getText())
			.put("paste[authorization]", "burger")
			.put("paste[restricted]", settings.privacy == PastiePrivacy.Private ? 1 : 0);
		
		return HttpUtil.executePost(PASTIE_URL, data, ResponseType.REDIRECT_URL);
	}
	
	public static class PastieSettings {
		@Setting(name = "Privacy", description = "Paste Privacy")
		public PastiePrivacy privacy = PastiePrivacy.Public;
	}
	
	private enum PastiePrivacy {
		Public, Private
	}
} @SettingsClass(SlexyUploader.SlexySettings.class)
public class SlexyUploader extends Uploader<TextUpload> {
	
	/**
	 * The Slexy submit url
	 */
	private static final String APIURL = "http://slexy.org/index.php/submit";
	
	/**
	 * The settings object used for this uploader
	 */
	private SlexySettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public SlexyUploader(SlexySettings settings) {
		this.settings = settings;
	}

	@Override
	public String upload(TextUpload t) throws Exception {
		RequestData data = new RequestData();
		
		data.put("raw_paste", t.getText())
			.put("author", settings.author != null ? settings.author : "")
			.put("comment", "")
			.put("desc", settings.description != null ? settings.description : "")
			.put("expire", FormatUtil.formattedTimeToSeconds(settings.expiration != null && !settings.expiration.isEmpty() ? settings.expiration : "0"))
			.put("language", "text")
			.put("linenumbers", settings.line_numbers)
			.put("permissions", settings.privacy == GenericPastePrivacy.Public ? 1 : 0)
			.put("submit", "Submit Paste")
			.put("tabbing", "true")
			.put("tabtype", "real");
		
		return HttpUtil.executePost(APIURL, data, ResponseType.REDIRECT_URL);
	}

	@Override
	public String getName() {
		return "Slexy.org";
	}
	
	public static class SlexySettings {
		//@Settings(required = {}, optional = { "author", "description", "visibility|combobox[Public,Private]", "line_numbers|checkbox[true]", "expiration|combobox[No expiration,5 minutes,15 minutes,30 minutes,1 hour,6 hours,12 hours,1 day,3 days,5 days,10 days,15 days,1 month,3 months,6 months]" })

		@Setting(name = "Author", description = "Paste Author", optional = true)
		public String author;
		
		@Setting(name = "Description", description = "Paste Description", optional = true)
		public String description;
		
		@Setting(name = "Privacy", description = "Paste Privacy", optional = true)
		public GenericPastePrivacy privacy = GenericPastePrivacy.Public;
		
		@Setting(name = "Line Numbers", description = "Show Line Numbers", defaults = "true", optional = true)
		public boolean line_numbers;
		
		@Setting(name = "Expiration", description = "Paste Expiration", type = ComboBoxSettingType.class, defaults = { "No expiration", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "1 day", "3 days", "5 days", "10 days", "15 days", "1 month", "3 months", "6 months" }, optional = true)
		public String expiration;
	}
} @SettingsClass(UpasteUploader.UpasteSettings.class)
public class UpasteUploader extends Uploader<TextUpload> {
	
	/**
	 * The API URL
	 */
	private static final String APIURL = "http://upaste.me/api";
	
	/**
	 * The API Key
	 */
	private static final String APIKEY = APICredentials.getCredential(UpasteUploader.class, "key");

	/**
	 * The settings object used for this uploader
	 */
	private UpasteSettings settings;
	
	/**
	 * Construct this uploader with the loaded settings
	 * @param settings
	 * 			The settings object
	 */
	public UpasteUploader(UpasteSettings settings) {
		this.settings = settings;
	}

	@Override
	public String upload(TextUpload t) throws Exception {
		RequestData data = new RequestData();
		
		data.put("api_key", settings.private_token != null && !settings.private_token.isEmpty() ? settings.private_token : APIKEY)
			.put("paste", t.getText())
			.put("name", settings.name != null ? settings.name : "")
			.put("privacy", settings.privacy == GenericPastePrivacy.Private ? 1 : 0)
			.put("expire", FormatUtil.formattedTimeToMinutes(settings.expiration != null && !settings.expiration.isEmpty() ? settings.expiration : "0"));
		
		String res = HttpUtil.executePost(APIURL, data);
		
		Map<String, String> m = Util.parseKeyValues(res, ": ", "\n");
		
		if(m.get("Status").equals("failure")) {
			throw new UploadException("Upload failed due to unknown reason.");
		}
		
		return m.get("Link");
	}

	@Override
	public String getName() {
		return "uPaste.me";
	}
	
	public static class UpasteSettings {
		@Setting(name = "Private Token", description = "Upaste Private Token", optional = true)
		public String private_token;
		
		@Setting(name = "Paste Name", description = "Paste Name", optional = true)
		public String name;
		
		@Setting(name = "Privacy", description = "Paste Privacy", optional = true)
		public GenericPastePrivacy privacy = GenericPastePrivacy.Public;
		
		@Setting(name = "Expiration", description = "Paste Expiration", type = ComboBoxSettingType.class, defaults = { "No expiration", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "1 day", "3 days", "5 days", "10 days", "15 days", "1 month", "3 months", "6 months" })
		public String expiration;
	}
} public class GoogleShortener extends Uploader<URLUpload> {

	/**
	 * The page url
	 */
	private static final String PAGE_URL = "https://www.googleapis.com/urlshortener/v1/url";

	@Override
	public String getName() {
		return "Goo.gl";
	}

	@Override
	public String upload(URLUpload url) throws Exception {
		//Sanity check, otherwise google's api returns a 400
		if(url.toString().matches("http://goo.gl/[a-zA-Z0-9]{1,10}")) {
			return url.toString();
		}
		
		URLConnection connection = new URL(PAGE_URL).openConnection();
		connection.setDoOutput(true);
		connection.setRequestProperty("Content-type", "application/json");
		
		JSONObject out = new JSONObject();
		
		out.put("longUrl", url.getURL());
		
		OutputStreamWriter writer = new OutputStreamWriter(
				connection.getOutputStream());
		writer.write(out.toString());
		writer.flush();
		writer.close();

		String contents = StreamUtils.readContents(connection.getInputStream());
		
		JSONObject resp = new JSONObject(contents);
		
		if(resp.has("id")) {
			return resp.getString("id");
		} else {
			throw new UploadException("Unable to find short url");
		}
	}
} public class IsgdShortener extends Uploader<URLUpload> {

	/**
	 * The page URL Format
	 */
	private static final String PAGE_URL = "http://is.gd/api.php";

	@Override
	public String upload(URLUpload url) throws Exception {
		RequestData data = new RequestData();
		
		data.put("longurl", url.getURL());
		
		String contents = HttpUtil.executeGet(PAGE_URL, data);
		
		if(contents.startsWith("http")) {
			return contents;
		}
		
		throw new UploadException("Unexpected response from server.");
	}

	@Override
	public String getName() {
		return "is.gd";
	}
} public class PostShortener extends Uploader<URLUpload> {

	/**
	 * The API KEY and URL
	 */
	private static final String API_KEY = APICredentials.getCredential(PostShortener.class, "key");
	
	private static final String API_URL = "http://po.st/api/shorten";

	@Override
	public String getName() {
		return "po.st";
	}

	@Override
	public String upload(final URLUpload url) throws Exception {
		RequestData data = new RequestData();
		
		data.put("longUrl", url.getURL())
			.put("apiKey", API_KEY)
			.put("format", "txt");

		final String shortURL = HttpUtil.executeGet(API_URL, data);
		
		if (shortURL.startsWith("http")) {
			return shortURL;
		}

		throw new UploadException("Unexpected response from server.");
	}

} public class TUrlShortener extends Uploader<URLUpload> {

	/**
	 * The base URL
	 */
	private static final String TURL_BASE = "http://turl.ca/";
	
	/**
	 * The API URL
	 */
	private static final String API_URL = TURL_BASE + "api.php";

	@Override
	public String getName() {
		return "TUrl.ca";
	}

	@Override
	public String upload(URLUpload url) throws Exception {
		RequestData data = new RequestData();
		
		data.put("url", url.getURL());
		
		// Request the page with the specified data
		String resp = HttpUtil.executeGet(API_URL, data);
		
		//Response is in STATUS:data format, data can be an error message or the shortened url.
		String status = resp.substring(0, resp.indexOf(':'));
		String resStr = resp.substring(resp.indexOf(':')+1);
		if (status.equalsIgnoreCase("ERROR")) {
			throw new UploadException(resStr);
		}
		return TURL_BASE + resStr;
	}
} public class TinyURLShortener extends Uploader<URLUpload> {

	/**
	 * The pattern to find the shortened url
	 */
	private static final Pattern urlPattern = Pattern
			.compile("<blockquote><b>(.*?)</b>");

	/**
	 * The page URL
	 */
	private static final String PAGE_URL = "http://tinyurl.com/create.php";

	@Override
	public String getName() {
		return "Tinyurl";
	}

	@Override
	public String upload(URLUpload url) throws Exception {
		RequestData data = new RequestData();
		
		data.put("url", url.getURL());
		
		String contents = HttpUtil.executeGet(PAGE_URL, data);
		
		Matcher matcher = urlPattern.matcher(contents);
		if (matcher.find()) {
			return matcher.group(1);
		} else {
			throw new UploadException("Cannot find the short url");
		}
	}
} public class ProgramOptions {

	/**
	 * Parse program arguments into a map
	 * @param args
	 * 			The arguments to prase
	 * @return
	 * 			The map of parsed arguments
	 */
	public static Map<String, Object> parseSettings(String[] args) {
		Map<String, Object> out = new HashMap<String, Object>();
		
		CmdLineParser parser = new CmdLineParser();
		
		Option<String> dir = parser.addStringOption('d', "dir");
		Option<String> language = parser.addStringOption('l', "language");
		Option<Boolean> resetConfig = parser.addBooleanOption("resetconfig");
		
		try {
			parser.parse(args);
		} catch (OptionException e) {
			return out;
		}
		
		if(dir.getOptionValue(parser) != null) {
			out.put("dir", dir.getOptionValue(parser));
		}
		
		if(language.getOptionValue(parser) != null) {
			out.put("language", dir.getOptionValue(parser));
		}
		
		if(resetConfig.getOptionValue(parser, false)) {
			out.put("resetconfig", true);
		}
		
		return out;
	}
} public class ScreenshotUtil {

	/**
	 * The robot instance
	 */
	private static Robot robot;

	/**
	 * Initialize it..
	 */
	static {
		try {
			robot = new Robot();
		} catch (AWTException e) {
			// We can't run without this!
			throw new RuntimeException(
					"Robot not initialized, shutting down...");
		}
	}

	/**
	 * Capture a simple screenshot
	 * 
	 * @return The screenshot
	 */
	public static BufferedImage capture() {
		return capture(DisplayUtil.getRealScreenSize());
	}

	/**
	 * Capture a screenshot
	 * 
	 * @param d
	 *            The dimensions of the area, starts at 0,0
	 * @return The captured image
	 */
	public static BufferedImage capture(Dimension d) {
		return capture(new Rectangle(0, 0, d.width, d.height));
	}

	/**
	 * Capture a regular screenshot, a static method for
	 * robot.createScreenCapture
	 * 
	 * @param rectangle
	 *            Rect to capture in screen coordinates
	 * @return The captured image
	 */
	public static BufferedImage capture(Rectangle rectangle) {
		return robot.createScreenCapture(rectangle);
	}
} public class Util {
	/**
	 * An enum containing operating system types
	 * 
	 * @author Nikki
	 * 
	 */
	public static enum OperatingSystem {
		LINUX, SOLARIS, WINDOWS, MAC, UNKNOWN
	}

	/**
	 * The cached operating system
	 */
	public static final OperatingSystem SYSTEM = getPlatform();

	/**
	 * The saved working directory
	 */
	private static File workDir;

	/**
	 * A method to get the unix time...
	 * @return
	 * 		The current time in seconds
	 */
	public static long currentTimeSeconds() {
		return (System.currentTimeMillis() / 1000);
	}

	/**
	 * Get the last part of a class name
	 * 
	 * @param key
	 *            The full name
	 * @return The class name formatted
	 */
	public static String formatClassName(Class<?> key) {
		return key.getName().substring(key.getName().lastIndexOf('.') + 1);
	}

	/**
	 * Get the computer's user agent
	 * 
	 */
	public static String getHttpUserAgent() {
		return Application.NAME + " v" + Version.getVersionString();
	}

	/**
	 * Get the current platform
	 * 
	 * @return The current platform
	 */
	public static OperatingSystem getPlatform() {
		String osName = System.getProperty("os.name").toLowerCase();
		if (osName.contains("win"))
			return OperatingSystem.WINDOWS;
		if (osName.contains("mac"))
			return OperatingSystem.MAC;
		if (osName.contains("solaris"))
			return OperatingSystem.SOLARIS;
		if (osName.contains("sunos"))
			return OperatingSystem.SOLARIS;
		if (osName.contains("linux"))
			return OperatingSystem.LINUX;
		if (osName.contains("unix"))
			return OperatingSystem.LINUX;
		return OperatingSystem.UNKNOWN;
	}

	/**
	 * Get a resource, allows us to run it from source or jar
	 * 
	 * @param name
	 *            The resource name
	 * @return The URL of the resource, either in-jar or on the filesystem
	 */
	public static URL getResourceByName(String name) {
		if (Utils.class.getResource(name) != null) {
			return Utils.class.getResource(name);
		} else {
			File file = null;
			if ((file = new File("resources" + name)).exists() || (file = new File(Util.getWorkingDirectory(), "resources" + name)).exists()) {
				try {
					return file.toURI().toURL();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
		return null;
	}

	/**
	 * Get the system architecture
	 * 
	 * @return The system architecture integer
	 */
	public static int getSystemArch() {
		String archs = System.getProperty("os.arch");
		return Integer.parseInt(archs.substring(archs.length() - 2));
	}

	/**
	 * Get the current working directory
	 * 
	 * @return The working directory for the application
	 */
	public static File getWorkingDirectory() {
		if (workDir == null)
			workDir = getWorkingDirectory(Constants.Application.NAME
					.toLowerCase());
		return workDir;
	}

	/**
	 * Get the AppData directory
	 * 
	 * @param applicationName
	 *            The application name
	 * @return The working directory
	 */
	public static File getWorkingDirectory(String applicationName) {
		String userHome = System.getProperty("user.home", ".");
		File workingDirectory;
		switch (getPlatform()) {
		case LINUX:
		case SOLARIS:
			File config = new File(userHome, ".config");
			if(config.exists()) {
				workingDirectory = new File(config, applicationName + '/');
				workingDirectory.mkdirs();
			} else {
				workingDirectory = new File(userHome, '.' + applicationName + '/');
			}
			break;
		case WINDOWS:
			String applicationData = System.getenv("APPDATA");
			if (applicationData != null)
				workingDirectory = new File(applicationData, "."
						+ applicationName + '/');
			else
				workingDirectory = new File(userHome,
						'.' + applicationName + '/');
			break;
		case MAC:
			workingDirectory = new File(userHome,
					"Library/Application Support/" + applicationName);
			break;
		default:
			workingDirectory = new File(userHome, applicationName + '/');
			break;
		}
		if ((!workingDirectory.exists()) && (!workingDirectory.mkdirs()))
			throw new RuntimeException(
					"The working directory could not be created: "
							+ workingDirectory);
		return workingDirectory;
	}

	/**
	 * Parse commandline arguments
	 * 
	 * @param args
	 *            The arg array from the main method, or manual args
	 * @return The map containing the args
	 */
	public static HashMap<String, Object> parseArguments(String[] args) {
		HashMap<String, Object> arguments = new HashMap<String, Object>();
		for (String s : args) {
			if (s.startsWith("--")) {
				s = s.substring(2);
			} else if (s.startsWith("-")) {
				s = s.substring(1);
			}
			int eqIdx = s.indexOf('=');
			String key = s.substring(0, eqIdx != -1 ? eqIdx : s.length());
			Object value = true;
			if (eqIdx != -1) {
				value = s.substring(s.indexOf('=') + 1);
			}
			arguments.put(key, value);
		}
		return arguments;
	}

	/**
	 * Set the working directory, used when Sleeksnap is self-contained (Uses a
	 * directory which you can have on a flash drive)
	 * 
	 * @param workingDirectory
	 *            The directory
	 */
	public static void setWorkingDirectory(File workingDirectory) {
		workDir = workingDirectory;
	}

	/**
	 * Trim the string's start/finish of the specified character
	 * 
	 * @param str
	 *            The string
	 * @param ch
	 *            The character
	 * @return The trimmed string
	 */
	public static String trim(String str, final char ch) {
		if ((str == null) || str.isEmpty())
			return str;
		else if (str.length() == 1)
			return str.charAt(0) == ch ? "" : str;
		try {
			if (str.charAt(0) == ch)
				str = str.substring(1);
			final int l = str.length() - 1;
			if (str.charAt(l) == ch)
				str = str.substring(0, l);
			return str;
		} catch (final Exception e) {
			return str;
		}
	}

	/**
	 * Make each word in a string uppercase
	 * 
	 * @param string
	 *            The string to parse
	 * @return The formatted string
	 */
	public static String ucwords(String string) {
		StringBuilder out = new StringBuilder();
		String[] split = string.split(" ");
		for (int i = 0; i < split.length; i++) {
			String str = split[i];
			out.append(Character.toUpperCase(str.charAt(0)));
			if (str.length() > 1) {
				out.append(str.substring(1).toLowerCase());
			}
			if (i < (split.length - 1)) {
				out.append(" ");
			}
		}
		return out.toString();
	}

	/**
	 * Center a frame on the main display
	 * @param frame
	 * 			The frame to center
	 */
	public static void centerFrameOnMainDisplay(JFrame frame) {
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice[] screens = ge.getScreenDevices();
		if (screens.length < 1) {
			return; // Silently fail.
		}
		Rectangle screenBounds = screens[0].getDefaultConfiguration().getBounds();
	    int x = (int) ((screenBounds.getWidth() - frame.getWidth()) / 2);
	    int y = (int) ((screenBounds.getHeight() - frame.getHeight()) / 2);
	    frame.setLocation(x, y);
	}

	/**
	 * A list of popular browsers
	 */
	private static final String[] BROWSERS = new String[] {
			"xdg-open", "google-chrome", "chromium", "firefox", "opera", "epiphany", "konqueror", "conkeror", "midori", "kazehakase", "mozilla"
	};

	/**
	 * A list of popular file managers
	 */
	private static final String[] FILE_MANAGERS = new String[] {
			"xdg-open", "nautilus", "dolphin", "thunar", "pcmanfm", "konqueror"
	};

	/**
	 * Finds the first supported program in the list (for UNIX-like platforms
	 * only).
	 * @param kind The kind of program, used in the exception message if no
	 * suitable program could be found.
	 * @param names The array of program names to try.
	 * @return The first supported program from the array of names.
	 * @throws Exception if no supported program could be found.
	 */
	private static String findSupportedProgram(String kind, String[] names) throws Exception {
		for (String name : names) {
			Process process = Runtime.getRuntime().exec(new String[] { "which", name });
			if (process.waitFor() == 0)
				return name;
		}

		throw new Exception("Unable to find supported " + kind);
	}

	/**
	 * Open a URL using java.awt.Desktop or a couple different manual methods
	 * @param url
	 * 			The URL to open
	 * @throws Exception
	 * 			If an error occurs attempting to open the url
	 */
	public static void openURL(URL url) throws Exception {
		Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
		if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
			desktop.browse(url.toURI());
		} else {
			OperatingSystem system = Util.getPlatform();
			switch (system) {
				case MAC:
					Class.forName("com.apple.eio.FileManager").getDeclaredMethod(
							"openURL", new Class[] { String.class }).invoke(null,
							new Object[] { url.toString() });
					break;
				case WINDOWS:
					Runtime.getRuntime().exec(new String[] { "rundll32", "url.dll,FileProtocolHandler", url.toString() });
					break;
				default:
					String browser = findSupportedProgram("browser", BROWSERS);
					Runtime.getRuntime().exec(new String[] { browser, url.toString() });
					break;
			}
		}
	}

	/**
	 * Open a file using {@link Desktop} if supported, or a manual
	 * platform-specific method if not.
	 * @param file The file to open.
	 * @throws Exception if the file could not be opened.
	 */
	public static void openFile(File file) throws Exception {
		Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
		if (desktop != null && desktop.isSupported(Desktop.Action.OPEN)) {
			desktop.open(file);
		} else {
			OperatingSystem system = Util.getPlatform();
			switch (system) {
				case MAC:
				case WINDOWS:
					openURL(file.toURI().toURL());
					break;
				default:
					String fileManager = findSupportedProgram("file manager", FILE_MANAGERS);
					Runtime.getRuntime().exec(new String[] { fileManager, file.getAbsolutePath() });
					break;
			}
		}
	}
	
	/**
	 * A utility method used for parsing uploader data (like what is used in the Spinner type)
	 * @param defaults
	 * 			The string to parse
	 * @return
	 * 			The data map
	 */
	public static Map<String, String> parseDataList(String[] defaults) {
		Map<String, String> ret = new HashMap<String, String>();
		for(String s : defaults) {
			int idx = s.indexOf('=');
			if(idx != -1) {
				String key = s.substring(0, idx);
				String value = s.substring(idx + 1);
				
				ret.put(key, value);
			} else {
				ret.put(s, null);
			}
		}
		return ret;
	}
	
	/**
	 * A utility method for parsing response data (Useful for k: v etc)
	 * @param input
	 * 			The input string
	 * @param kvSeparator
	 * 			The value separating key and value
	 * @param entrySeparator
	 * 			The value separating entries
	 * @return
	 * 			The parsed map
	 */
	public static Map<String, String> parseKeyValues(String input, String kvSeparator, String entrySeparator) {
		Map<String, String> ret = new HashMap<String, String>();
		String[] split = input.split(entrySeparator);
		for(String s : split) {
			int idx = s.indexOf(kvSeparator);
			if(idx != -1) {
				String key = s.substring(0, idx);
				String value = s.substring(idx + 1);
				
				ret.put(key, value);
			} else {
				ret.put(s, null);
			}
		}
		return ret;
	}

	/**
	 * Implode a list of values (Like php's implode)
	 * @param list
	 * 			The list to implode
	 * @param glue
	 * 			The glue to use between values
	 * @return
	 * 			The imploded string
	 */
	public static String implodeList(List<?> list, String glue) {
		StringBuilder builder = new StringBuilder();
		for(Iterator<?> it = list.iterator(); it.hasNext();) {
			builder.append(it.next());
			if(it.hasNext()) {
				builder.append(glue);
			}
		}
		return builder.toString();
	}

	/**
	 * Gets the class name of the Swing look and feel which emulates the native
	 * style of the operating system.
	 * @return The system look and feel class name.
	 */
	public static String getSystemLookAndFeelClassName() {
		OperatingSystem platform = getPlatform();
		String clazz = UIManager.getSystemLookAndFeelClassName();
		
		/*
		 * On Linux, Java only attempts to use the GTK+ style if the desktop
		 * environment is GNOME. Otherwise, it falls back to the cross-platform
		 * style.
		 * 
		 * However, many other desktop environments use GTK+ (e.g. XFCE, LXDE,
		 * Unity, etc.) and even ones that don't (e.g. KDE) usually have some
		 * GTK+ compatibility so it still looks better than the cross-platform
		 * theme.
		 * 
		 * Therefore on Linux we override Java's choice and attempt to use GTK+
		 * if the cross-platform style was chosen.
		 */
		if (platform == OperatingSystem.LINUX && clazz.equals(UIManager.getCrossPlatformLookAndFeelClassName())) {
			clazz = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel";
		}
		
		return clazz;
	}
} public static class ClipboardUtil {
	@SuppressWarnings("serial")
	public static class ClipboardException extends Exception {
		public ClipboardException(Exception e) {
			super(e);
		}
	}

	/**
	 * The clipboard instance
	 */
	private static Clipboard clipboard = Toolkit.getDefaultToolkit()
			.getSystemClipboard();

	@SuppressWarnings("unchecked")
	/**
	 * Get the clipboard contents
	 * @return
	 * 			Either an Image, String or File
	 * @throws ClipboardException
	 * 			If an error occurred
	 */
	public static Object getClipboardContents() throws ClipboardException {
		Transferable contents = clipboard.getContents(null);
		if (contents.isDataFlavorSupported(DataFlavor.imageFlavor)) {
			// An image is on the clipboard!
			try {
				// Get the image from the clipboard
				Image image = (Image) contents
						.getTransferData(DataFlavor.imageFlavor);
				if (image != null) {
					return image;
				}
			} catch (Exception e) {
				throw new ClipboardException(e);
			}
		} else if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
			// Text is on the clipboard!
			try {
				// Get the string from the clipboard
				String string = (String) contents
						.getTransferData(DataFlavor.stringFlavor);
				if (string != null) {
					return string;
				}
			} catch (Exception e) {
				throw new ClipboardException(e);
			}
		} else if (contents
				.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
			// Files are on the clipboard
			try {
				// We only will use the first file...
				List<File> list = (List<File>) contents
						.getTransferData(DataFlavor.javaFileListFlavor);
				if (list.size() > 0) {
					return list.get(0);
				}
			} catch (Exception e) {
				throw new ClipboardException(e);
			}
		}
		return null;
	}

	/**
	 * Set the clipboard contents to a string
	 * 
	 * @param string
	 *            The contents
	 */
	public static void setClipboard(String string) {
		clipboard.setContents(new StringSelection(string), null);
	}
} @SuppressWarnings("serial")
public static class ClipboardException extends Exception {
	public ClipboardException(Exception e) {
		super(e);
	}
} public static class DateUtil {

	/**
	 * The date format
	 */
	private static SimpleDateFormat dateFormat = new SimpleDateFormat(
			"MM-dd-yyyy_HH-mm-ss");

	/**
	 * Gets a filename friendly date
	 * 
	 * @return The formatted date
	 */
	public static String getCurrentDate() {
		return dateFormat.format(new Date());
	}
} public static class DisplayUtil {

	/**
	 * Get the real screen size, multiple screens..
	 * 
	 * @return The screen size
	 */
	public static Rectangle getRealScreenSize() {
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice[] screens = ge.getScreenDevices();

		Rectangle allScreenBounds = new Rectangle();
		for (GraphicsDevice screen : screens) {
			Rectangle screenBounds = screen.getDefaultConfiguration().getBounds();

			allScreenBounds.width += screenBounds.width;
			allScreenBounds.height = Math.max(allScreenBounds.height, screenBounds.height);

			if (screenBounds.x < allScreenBounds.y || screenBounds.y < allScreenBounds.y) {
				allScreenBounds.x = Math.min(allScreenBounds.x, screenBounds.x);
				allScreenBounds.y = Math.min(allScreenBounds.y, screenBounds.y);
			}
		}
		return allScreenBounds;
	}

	/**
	 * Get the real screen size, multiple screens..
	 * 
	 * @return The screen size
	 */
	public static Rectangle[] getAllScreenBounds() {
		GraphicsEnvironment ge = GraphicsEnvironment
				.getLocalGraphicsEnvironment();
		GraphicsDevice[] screens = ge.getScreenDevices();

		Rectangle[] allScreenBounds = new Rectangle[screens.length];
		for (int i = 0; i < screens.length; i++) {
			allScreenBounds[i] = screens[i].getDefaultConfiguration().getBounds();
		}
		return allScreenBounds;
	}
} public static class FileUtils {

	/**
	 * Get the mime type for a file
	 * 
	 * @param fileUrl
	 *            The file url (Can use file.toURI().toURL())
	 * @return The mime type
	 */
	public static String getMimeType(String fileUrl) {
		FileNameMap fileNameMap = URLConnection.getFileNameMap();
		String type = fileNameMap.getContentTypeFor(fileUrl);

		return type;
	}

	/**
	 * Read a file
	 * 
	 * @param file
	 *            The file
	 * @return The contents
	 * @throws IOException
	 *             If an error occurred
	 */
	public static String readFile(File file) throws IOException {
		StringBuilder contents = new StringBuilder();
		BufferedReader reader = new BufferedReader(new FileReader(file));
		try {
			while (true) {
				String line = reader.readLine();
				if (line == null) {
					break;
				}
				contents.append(line).append("\n");
			}
		} finally {
			reader.close();
		}
		return contents.toString();
	}
		
	/**
	 * Get the jar running from as a File object
	 * 
	 * @param cl
	 * 			The class to get the path from
	 * @return
	 * 			A File object representing the path
	 * @throws Exception
	 * 			If the path is invalid
	 */
	public static File getJarFile(Class<?> cl) throws Exception {
		return new File(cl.getProtectionDomain().getCodeSource()
				.getLocation().toURI());
	}

	/**
	 * Get the jar path
	 * 
	 * @param cl
	 *            The class to get the path from
	 * @return The path
	 * @throws Exception
	 *             If the path is invalid
	 */
	public static String getJarPath(Class<?> cl) throws Exception {
		return getJarFile(cl).getAbsolutePath();
	}

	/**
	 * Generate a unique filename for uploads etc
	 * 
	 * @param extension
	 *            The file extension
	 * @return The generated filename
	 */
	public static String generateFileName(String extension) {
		return Application.NAME + "-" + DateUtil.getCurrentDate()
				+ (extension.equals("") ? "" : "." + extension);
	}

	/**
	 * Generate a file name from a date object formatted for filenames, plus the
	 * filename if applicable
	 * 
	 * @param upload
	 *            The object to be uploaded
	 * @return The filename
	 */
	public static String generateFileName(Upload upload) {
		String name = DateUtil.getCurrentDate();
		if (upload instanceof ImageUpload) {
			name += ".png";
		} else if (upload instanceof TextUpload) {
			name += ".txt";
		} else if (upload instanceof FileUpload) {
			name = ((FileUpload) upload).getFile().getName();
		} else {
			name += ".file";
		}
		return name;
	}
		
	/**
	 * Find the java executable for the current java runtime
	 * 
	 * @return
	 * 		The File of the java executable, or null if it was not found (This is very bad)
	 */
	public static File getJavaExecutable() {
		File javaDir = new File(System.getProperty("java.home"));
			
		String[] exes = new String[] { "bin/javaw", "bin/java" };
			
		boolean isWindows = Platform.isWindows();
			
		for(String s : exes) {
			if(isWindows) {
				s = s + ".exe";
			}
			File f = new File(javaDir, s);
			if(f.exists()) {
				return f;
			}
		}
		return null;
	}
} public class WindowUtilProvider {
	/**
	 * The WindowUtil currently in use
	 */
	private static WindowUtil cachedUtil = null;

	/**
	 * Get the current WindowUtil
	 * 
	 * @return The WindowUtil for the OS
	 */
	public static WindowUtil getWindowUtil() {
		if (cachedUtil == null) {
			if (Platform.isWindows()) {
				cachedUtil = new Win32WindowUtil();
			} else if (Platform.isLinux()) {
				if (GnomeWindowUtil.isValid()) {
					cachedUtil = new GnomeWindowUtil();
				} else {
					cachedUtil = new XPropWindowUtil();
				}
			}
		}
		return cachedUtil;
	}
} public class LogPanelHandler extends java.util.logging.Handler {

	private static LogPanel logPanel;

	/**
	 * A simple file logger!
	 */
	public LogPanelHandler() {
	}

	@Override
	public void publish(LogRecord record) {
		if(logPanel == null) {
			return;
		}
		if (getFormatter() == null)
			setFormatter(new SimpleFormatter());
		
		if (!isLoggable(record))
			return;
		
		logPanel.appendLog(getFormatter().format(record));
	}

	@Override
	public void flush() {
	}

	@Override
	public void close() throws SecurityException {
	}

	/**
	 * Bind this logging handler to a LogPanel instance to update the log screen
	 * @param logPanel
	 * 			The panel to bind
	 */
	public static void bindTo(LogPanel logPanel) {
		LogPanelHandler.logPanel = logPanel;
	}

	/**
	 * Unbind the LogPanel instance (Always done when the settings gui is closed)
	 */
	public static void unbind() {
		LogPanelHandler.logPanel = null;
	}
}
import java.awt.AWTException;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Constructor;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.sleeksnap.Constants.Application;
import org.sleeksnap.Constants.Resources;
import org.sleeksnap.Constants.Version;
import org.sleeksnap.filter.FilterException;
import org.sleeksnap.filter.PNGCompressionFilter;
import org.sleeksnap.filter.UploadFilter;
import org.sleeksnap.filter.WatermarkFilter;
import org.sleeksnap.gui.OptionPanel;
import org.sleeksnap.gui.SelectionWindow;
import org.sleeksnap.impl.History;
import org.sleeksnap.impl.HistoryEntry;
import org.sleeksnap.impl.HotkeyManager;
import org.sleeksnap.impl.Language;
import org.sleeksnap.impl.LoggingManager;
import org.sleeksnap.updater.Updater;
import org.sleeksnap.updater.Updater.VerificationMode;
import org.sleeksnap.updater.UpdaterMode;
import org.sleeksnap.updater.UpdaterReleaseType;
import org.sleeksnap.upload.FileUpload;
import org.sleeksnap.upload.ImageUpload;
import org.sleeksnap.upload.TextUpload;
import org.sleeksnap.upload.URLUpload;
import org.sleeksnap.upload.Upload;
import org.sleeksnap.uploaders.Uploader;
import org.sleeksnap.uploaders.UploaderConfigurationException;
import org.sleeksnap.uploaders.UploaderLoader;
import org.sleeksnap.uploaders.files.FilebinUploader;
import org.sleeksnap.uploaders.files.UppitUploader;
import org.sleeksnap.uploaders.generic.FTPUploader;
import org.sleeksnap.uploaders.generic.GenericUploader;
import org.sleeksnap.uploaders.generic.LocalFileUploader;
import org.sleeksnap.uploaders.generic.LocalFileUploader.ImageLocalFileUploader;
import org.sleeksnap.uploaders.images.ImageScriptUploader;
import org.sleeksnap.uploaders.images.ImagebinUploader;
import org.sleeksnap.uploaders.images.ImgurUploader;
import org.sleeksnap.uploaders.images.ImmioUploader;
import org.sleeksnap.uploaders.images.PuushUploader;
import org.sleeksnap.uploaders.settings.SettingsClass;
import org.sleeksnap.uploaders.text.LaravelUploader;
import org.sleeksnap.uploaders.text.Paste2Uploader;
import org.sleeksnap.uploaders.text.PastebinUploader;
import org.sleeksnap.uploaders.text.PastebincaUploader;
import org.sleeksnap.uploaders.text.PasteeUploader;
import org.sleeksnap.uploaders.text.PastieUploader;
import org.sleeksnap.uploaders.text.SlexyUploader;
import org.sleeksnap.uploaders.text.UpasteUploader;
import org.sleeksnap.uploaders.url.GoogleShortener;
import org.sleeksnap.uploaders.url.IsgdShortener;
import org.sleeksnap.uploaders.url.PostShortener;
import org.sleeksnap.uploaders.url.TUrlShortener;
import org.sleeksnap.uploaders.url.TinyURLShortener;
import org.sleeksnap.util.ProgramOptions;
import org.sleeksnap.util.ScreenshotUtil;
import org.sleeksnap.util.Util;
import org.sleeksnap.util.Utils.ClipboardUtil;
import org.sleeksnap.util.Utils.ClipboardUtil.ClipboardException;
import org.sleeksnap.util.Utils.DateUtil;
import org.sleeksnap.util.Utils.DisplayUtil;
import org.sleeksnap.util.Utils.FileUtils;
import org.sleeksnap.util.active.WindowUtilProvider;
import org.sleeksnap.util.logging.LogPanelHandler;
import com.google.gson.Gson;
import com.sun.jna.Platform;
/**
 * Sleeksnap, the open source cross-platform screenshot uploader
 * Copyright (C) 2012 Nikki <nikki@nikkii.us>
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sleeksnap;





/**
 * The main Uploader Utility class
 * 
 * @author Nikki
 * 
 */
public class ScreenSnapper {

	/**
	 * A basic class which lets us execute a custom action based on the
	 * ScreenshotAction class
	 * 
	 * @author Nikki
	 * 
	 */
	@SuppressWarnings("serial")
	private class ActionMenuItem extends MenuItem implements ActionListener {
		/**
		 * The action id
		 */
		private int action;

		public ActionMenuItem(String s, int action) {
			super(s);
			this.action = action;
			addActionListener(this);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			hotkey(action);
		}
	}

	/**
	 * A wrapper for action ids
	 * 
	 * @author Nikki
	 * 
	 */
	public static class ScreenshotAction {
		private static final int CROP = 1;
		private static final int FULL = 2;
		private static final int CLIPBOARD = 3;
		public static final int ACTIVE = 4;
		public static final int FILE = 5;
	}

	/**
	 * A basic hack for class associations -> names
	 */
	private static HashMap<Class<?>, String> names = new HashMap<Class<?>, String>();

	/**
	 * Load the names and set the useragent
	 */
	static {
		System.setProperty("http.agent", Util.getHttpUserAgent());

		names.put(ImageUpload.class, "Images");
		names.put(TextUpload.class, "Text");
		names.put(URLUpload.class, "Urls");
		names.put(FileUpload.class, "Files");
		
		// Register the Password serializer/deserializer
	}

	/**
	 * Logging instance
	 */
	private static final Logger logger = Logger.getLogger(ScreenSnapper.class.getName());

	public static void main(String[] args) {
		// Initialize program
		ScreenSnapper instance = new ScreenSnapper();
		instance.initialize(ProgramOptions.parseSettings(args));
	}

	/**
	 * A map which contains uploader classes -> a list of available uploaders
	 */
	private HashMap<Class<? extends Upload>, Map<String, Uploader<? extends Upload>>> uploaders = new HashMap<Class<? extends Upload>, Map<String, Uploader<? extends Upload>>>();

	/**
	 * A map which contains the current uploader settings
	 */
	private HashMap<Class<? extends Upload>, Uploader<?>> uploaderAssociations = new HashMap<Class<? extends Upload>, Uploader<?>>();

	/**
	 * A map containing upload filters
	 */
	private HashMap<Class<? extends Upload>, LinkedList<UploadFilter<?>>> filters = new HashMap<Class<? extends Upload>, LinkedList<UploadFilter<?>>>();

	/**
	 * The basic service...
	 */
	private ExecutorService serv = Executors.newSingleThreadExecutor();

	/**
	 * The ExecutorService used to upload
	 */
	private ExecutorService uploadService = Executors.newSingleThreadExecutor();

	/**
	 * The tray icon
	 */
	private TrayIcon icon;

	/**
	 * The configuration instance
	 */
	private Configuration configuration = new Configuration();

	/**
	 * The selection window instances
	 */
	private SelectionWindow window = null;

	/**
	 * Defines whether the options panel is open
	 */
	private boolean optionsOpen;

	/**
	 * The hotkey manager instance
	 */
	private HotkeyManager keyManager;

	/**
	 * The history instance
	 */
	private History history;

	/**
	 * The last uploaded URL, used for clicking tray icon
	 */
	private String lastUrl;

	/**
	 * The number of retries used
	 */
	private int retries;

	/**
	 * The open settings window
	 */
	private JFrame settingsFrame;

	/**
	 * Initialize the program
	 * 
	 * @param map
	 *            Flag to reset configuration
	 */
	private void initialize(Map<String, Object> map) {
		// Check for a configuration option
		if (map.containsKey("dir")) {
			File file = new File(map.get("dir").toString());
			if (!file.exists()) {
				file.mkdirs();
			}
			Util.setWorkingDirectory(file);
		}
		// Verify the directory
		File local = Util.getWorkingDirectory();
		if (!local.exists()) {
			local.mkdirs();
		}
		// Load the Release info
		URL releaseUrl = Util.getResourceByName("/org/sleeksnap/release.json");
		JSONObject releaseInfo = new JSONObject();
		if(releaseUrl != null) {
			try {
				releaseInfo = new JSONObject(new JSONTokener(releaseUrl.openStream()));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		// Set the UI skin
		try {
			UIManager.setLookAndFeel(releaseInfo.getString("uiClass", Util.getSystemLookAndFeelClassName()));
		} catch (Exception e) {
			e.printStackTrace();
		}
		// Then start
		try {
			LoggingManager.configure();
		} catch(IOException e) {
			logger.log(Level.WARNING, "Unable to configure logger, file logging and logging panel will not work.", e);
			JOptionPane.showMessageDialog(null, "Unable to configure logger, file logging and logging panel will not work.", "Error", JOptionPane.ERROR_MESSAGE);
		}
		logger.info("Loading plugins...");
		try {
			loadUploaders();
			loadFilters();
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Failed to load plugins!", e);
		}
		// Load the settings
		logger.info("Loading settings...");
		try {
			loadSettings(map.containsKey("resetconfig"));
		} catch (Exception e) {
			logger.log(Level.SEVERE, "Failed to load settings!", e);
		}
		// Load the selected language
		try {
			Language.load(map.containsKey("language") ? map.get("language").toString() : configuration.getString("language", Constants.Configuration.DEFAULT_LANGUAGE));
		} catch (IOException e) {
			logger.log(Level.SEVERE, "Failed to load language file!", e);
		}
		// Check the update mode
		UpdaterMode mode = configuration.getEnumValue("updateMode", UpdaterMode.class);