package com.github.sommeri.less4j; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collection; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import com.github.sommeri.less4j.utils.URIUtils; public abstract class LessSource { public abstract LessSource relativeSource(String filename) throws FileNotFound, CannotReadFile, StringSourceException; public abstract String getContent() throws FileNotFound, CannotReadFile; public abstract byte[] getBytes() throws FileNotFound, CannotReadFile; /** * @return less source location uri or <code>null</code>. If non-null, last path part must be equal to whatever {@link #getName()} returns. * */ public URI getURI() { return null; } /** * Less source name. Can be anything if the {@link #getAbsoluteURI()} returns non null, otherwise last path part. */ public String getName() { return null; } public abstract static class AbstractHierarchicalSource extends LessSource { protected AbstractHierarchicalSource parent; protected long lastModified; protected long latestModified; protected Collection<LessSource> importedSources; public AbstractHierarchicalSource() { super(); importedSources = new ArrayList<LessSource>(); } public AbstractHierarchicalSource(AbstractHierarchicalSource parent) { super(); this.parent = parent; } protected void setLastModified(long lastModified) { this.lastModified = lastModified; setLatestModified(lastModified); } protected void addImportedSource(LessSource source) { if (parent != null) { parent.addImportedSource(source); } else { importedSources.add(source); } } public Collection<LessSource> getImportedSources() { return importedSources; } public long getLastModified() { return lastModified; } public long getLatestModified() { return latestModified; } public void setLatestModified(long latestModified) { this.latestModified = latestModified; if (parent != null && latestModified > parent.getLatestModified()) { parent.setLatestModified(latestModified); } } } public static class URLSource extends AbstractHierarchicalSource { private URL inputURL; private String charsetName; public URLSource(URL inputURL) { this(inputURL, null); } public URLSource(URL inputURL, String charsetName) { super(); this.inputURL = inputURL; this.charsetName = charsetName; } public URLSource(URLSource parent, String filename) throws FileNotFound, CannotReadFile { this(parent, filename, null); } public URLSource(URLSource parent, String filename, String charsetName) throws FileNotFound, CannotReadFile { super(parent); try { this.inputURL = new URL(URIUtils.toParentURL(parent.inputURL), filename); } catch (MalformedURLException e) { throw new FileNotFound(); } this.charsetName = charsetName; parent.addImportedSource(this); } @Override public String getContent() throws FileNotFound, CannotReadFile { try { URLConnection connection = getInputURL().openConnection(); Reader input = charsetName != null ? new InputStreamReader(connection.getInputStream(), charsetName) : new InputStreamReader(connection.getInputStream()); String content = IOUtils.toString(input).replace("\r\n", "\n"); setLastModified(connection.getLastModified()); input.close(); return content; } catch (FileNotFoundException ex) { throw new FileNotFound(); } catch (IOException ex) { throw new CannotReadFile(); } } @Override public byte[] getBytes() throws FileNotFound, CannotReadFile { try { URLConnection connection = getInputURL().openConnection(); Reader input = new InputStreamReader(connection.getInputStream()); setLastModified(connection.getLastModified()); byte[] content = IOUtils.toByteArray(input); input.close(); return content; } catch (FileNotFoundException ex) { throw new FileNotFound(); } catch (IOException ex) { throw new CannotReadFile(); } } @Override public LessSource relativeSource(String filename) throws FileNotFound, CannotReadFile { return new URLSource(this, filename, charsetName); } public URL getInputURL() { return inputURL; } @Override public URI getURI() { try { return inputURL.toURI(); } catch (URISyntaxException e) { return null; } } @Override public String getName() { return new File(inputURL.getPath()).getName(); } public String toString() { return inputURL.toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((inputURL == null) ? 0 : inputURL.hashCode()); result = prime * result + ((charsetName == null) ? 0 : charsetName.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; URLSource other = (URLSource) obj; if (inputURL == null) { if (other.inputURL != null) return false; } else if (!inputURL.equals(other.inputURL)) return false; if (charsetName == null) { if (other.charsetName != null) return false; } else if (!charsetName.equals(other.charsetName)) return false; return true; } } public static class FileSource extends AbstractHierarchicalSource { private static String DEFAULT_CHARSET = "utf-8"; private File inputFile; private String charsetName; public FileSource(File inputFile) { this(inputFile, DEFAULT_CHARSET); } public FileSource(File inputFile, String charsetName) { this.inputFile = inputFile; this.charsetName = charsetName; } public FileSource(FileSource parent, String filename) { this(parent, filename, DEFAULT_CHARSET); } public FileSource(FileSource parent, String filename, String charsetName) { this(parent, new File(parent.getInputFile().getParentFile(), filename), charsetName); } public FileSource(FileSource parent, File inputFile, String charsetName) { super(parent); this.inputFile = inputFile; this.charsetName = charsetName; parent.addImportedSource(this); } @Override public URI getURI() { try { String path = getInputFile().toString(); path = URIUtils.convertPlatformSeparatorToUri(path); return new URI(path); } catch (URISyntaxException e) { return null; } } @Override public String getName() { return getInputFile().getName(); } @Override public String getContent() throws FileNotFound, CannotReadFile { try { Reader input; if (charsetName != null) { input = new InputStreamReader(new FileInputStream(getInputFile()), charsetName); } else { input = new FileReader(getInputFile()); } try { String content = IOUtils.toString(input).replace("\r\n", "\n"); setLastModified(getInputFile().lastModified()); return content; } finally { input.close(); } } catch (FileNotFoundException ex) { throw new FileNotFound(); } catch (IOException ex) { throw new CannotReadFile(); } } @Override public byte[] getBytes() throws FileNotFound, CannotReadFile { try { byte[] content = FileUtils.readFileToByteArray(getInputFile()); setLastModified(getInputFile().lastModified()); return content; } catch (FileNotFoundException ex) { throw new FileNotFound(); } catch (IOException ex) { throw new CannotReadFile(); } } @Override public FileSource relativeSource(String filename) { return new FileSource(this, filename, charsetName); } public File getInputFile() { return inputFile; } public String toString() { return getInputFile().toString(); } @Override public int hashCode() { final int prime = 31; int result = 1; String canonicalInputFile = getCanonicalPath(); result = prime * result + ((canonicalInputFile == null) ? 0 : canonicalInputFile.hashCode()); result = prime * result + ((charsetName == null) ? 0 : charsetName.hashCode()); return result; } private String getCanonicalPath() { try { return getInputFile().getCanonicalPath(); } catch (IOException e) { return getInputFile().getAbsolutePath(); } } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; FileSource other = (FileSource) obj; String absoluteInputFile = getCanonicalPath(); if (absoluteInputFile == null) { if (other.getInputFile() != null) return false; } else if (!absoluteInputFile.equals(other.getCanonicalPath())) return false; if (charsetName == null) { if (other.charsetName != null) return false; } else if (!charsetName.equals(other.charsetName)) return false; return true; } } public static class StringSource extends LessSource { private String content; private String name; private URI uri; public StringSource(String content) { this(content, null); } public StringSource(String content, String name) { this.content = content; this.name = name; } public StringSource(String content, String name, URI uri) { this.content = content; this.name = name; this.uri = uri; } @Override public URI getURI() { return uri; } @Override public String getName() { return name; } @Override public String getContent() { return content; } @Override public byte[] getBytes() { return content!=null?content.getBytes():null; } @Override public LessSource relativeSource(String filename) throws StringSourceException { throw new StringSourceException(); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((content == null) ? 0 : content.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; StringSource other = (StringSource) obj; if (content == null) { if (other.content != null) return false; } else if (!content.equals(other.content)) return false; return true; } } @SuppressWarnings("serial") public static class StringSourceException extends Exception { } @SuppressWarnings("serial") public static class FileNotFound extends Exception { } @SuppressWarnings("serial") public static class CannotReadFile extends Exception { } }