diff --git a/Contributing.txt b/Contributing.txt new file mode 100644 index 0000000..61b1baa --- /dev/null +++ b/Contributing.txt @@ -0,0 +1,35 @@ +This software is distributed under a permissive open source +license to allow it to be used in any projects, whether open +source or proprietary. Contributions to the project are welcome +and it is important to maintain clear record of contributions +and terms under which they are licensed. + +To indicate your acceptance of Developer's Certificate of Origin 1.1 +terms, please add the following line to the end of the commit message +for each contribution you make to the project: + +Signed-off-by : Your Name + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or inpart by me and I +have the right to submit it under the open source license indicated +in the file: or + +(b) The contribution is based upon previous work that, to the best +of my knowledge, is covered under an appropriate open source license +and I have the right under that license to submit that work with +modifications, whether created in whole or part by me, under the same +open source license (unless I am permitted to submit under a different +license), as indicated in the file; or + +(c) The contribution was provided directly to me by some other person +who certified (a), (b) or (c) I have not modified it. + +(d) I understand and agree that this project and the contribution are +public and that a record of the contribution (including all personal +information I submit with it, including my sign-off)is maintained +indefinitely and may be redistributed consistent with this project or +the open source license(s) involved. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..db5306f --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +BSD License + +Copyright (c) <2016>, AT&T Intellectual Property. All other rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions + and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of + conditions and the following disclaimer in the documentation and/or other materials provided + with the distribution. +3. All advertising materials mentioning features or use of this software must display the + following acknowledgement: This product includes software developed by the AT&T. +4. Neither the name of AT&T nor the names of its contributors may be used to endorse or + promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY AT&T INTELLECTUAL PROPERTY ''AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL AT&T INTELLECTUAL PROPERTY BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. \ No newline at end of file diff --git a/saserverlibrary/pom.xml b/saserverlibrary/pom.xml new file mode 100644 index 0000000..b9f05ee --- /dev/null +++ b/saserverlibrary/pom.xml @@ -0,0 +1,198 @@ + + + + 4.0.0 + com.att.nsa + nsaServerLibrary + 1.0.10 + jar + + Network Service Assurance Server Library + Library of code used in various API servers + http://attgfp.net + + + 1.7 + 1.7 + + 2.3.1 + 2.1.1 + + + + scm:git:ssh://git@codecloud.web.att.com:7999/st_2020int/saserverlibrary.git + scm:git:ssh://git@codecloud.web.att.com:7999/st_2020int/saserverlibrary.git + https://codecloud.web.att.com/scm/st_2020int/saserverlibrary.git + + + + + javax.servlet.jsp + jsp-api + 2.2 + provided + + + org.slf4j + slf4j-api + 1.7.5 + + + junit + junit + 4.11 + test + + + javax.mail + mail + 1.4 + + + + com.att.nsa + saToolkit + 0.0.1 + + + + + com.att.nsa + cambriaClient + 0.0.1 + + + + + + + + org.json + json + 20131018 + + + + + org.apache.velocity + velocity + 1.7 + + + commons-io + commons-io + 2.4 + + + + + jline + jline + 2.11 + true + + + + + org.apache.tomcat + tomcat-catalina + 8.0.36 + + + org.apache.tomcat + tomcat-util + 8.0.36 + + + org.apache.tomcat.embed + tomcat-embed-core + 8.0.36 + + + + + com.datastax.cassandra + cassandra-driver-core + 2.1.0 + true + + + + + org.apache.zookeeper + zookeeper + 3.3.2 + true + + + com.sun.jmx + jmxri + + + com.sun.jdmk + jmxtools + + + javax.jms + jms + + + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + + + + + + com.101tec + zkclient + 0.3 + true + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + + + + + + + + + jenkins + + + env.BUILD_NUMBER + + + + + + org.codehaus.mojo + cobertura-maven-plugin + + + + + + diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServer.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServer.java new file mode 100644 index 0000000..1641fa8 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServer.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.Servlet; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleListener; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.startup.Tomcat; +import org.apache.coyote.http11.Http11NioProtocol; + +public class ApiServer { + + private final Tomcat tomcat; + private final Servlet servlet; + private final String name; + + private ApiServer(Builder builder) throws IOException { + + if (builder.encodeSlashes()) { + // Tell Tomcat that we want encoded slashes in our paths. This isn't normally + // used in the Cambria system, but some metrics require it. + System.setProperty ( + "org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", + "true" ); + } + + this.servlet = builder.servlet(); + this.name = builder.name(); + + tomcat = new Tomcat(); + + final String servletName = name + "ApiServlet"; + final Context rootCtx = tomcat.addContext("", makeTmpDir(name + "Context").getAbsolutePath()); + Tomcat.addServlet(rootCtx, servletName, servlet); + rootCtx.addServletMapping ( "/*", servletName); + + final List userConnectors = builder.connectors (); + if ( userConnectors.size () > 0 ) + { + // remove the default connector, which isn't actually created yet, but will be, + // because tomcat is a pretty sloppy piece of software. to trigger the creation + // of the default connector now, we need to get it once. + + boolean doneOne = false; + for (ApiServerConnector connector : userConnectors ) { + + final Connector conn = new Connector ( builder.protocolClassName ); + if ( connector.isSecure () ) + { + conn.setScheme ( "https" ); + conn.setSecure ( true ); + conn.setAttribute ( "keystorePass", connector.keystorePassword () ); + conn.setAttribute ( "keystoreFile", connector.keystoreFile () ); + conn.setAttribute ( "keyAlias", connector.keyAlias () ); + conn.setAttribute ( "clientAuth", "false" ); + conn.setAttribute ( "sslProtocol", "TLS" ); + conn.setAttribute ( "SSLEnabled", true ); + } + conn.setPort ( connector.port () ); + + // connection timeouts. NOTE: tomcat has some code (commented as hacky) + // that decides to use the keepAlive timeout instead of the connection + // timeout for reading the initial HTTP line. that defeats our connection + // timeout for bogus connections, so we have to use a keepAlive that matches. + conn.setAttribute ( "socket.soTimeout", connector.socketTimeoutMs () ); + conn.setAttribute ( "connectionTimeout", connector.socketTimeoutMs () ); +// conn.setAttribute ( "keepAliveTimeout", connector.keepAliveTimeoutMs () ); +// conn.setAttribute ( "socket.soKeepAlive", connector.keepAliveTimeoutMs () ); + conn.setAttribute ( "keepAliveTimeout", connector.socketTimeoutMs () ); + conn.setAttribute ( "socket.soKeepAlive", connector.socketTimeoutMs () ); + + if (connector.maxThreads() > 0) conn.setAttribute("maxThreads", connector.maxThreads()); + + tomcat.getService().addConnector(conn); + if ( !doneOne ) + { + // replace tomcat's default connector + tomcat.setConnector ( conn ); + } + doneOne = true; + } + } + // else: leave default connector on 8080 + } + + public static class Builder { + + private final List connectors; + private final Servlet servlet; + private String protocolClassName = Http11NioProtocol.class.getName (); + private String name = "apiServer"; + private boolean encodeSlashes = false; + + public Builder(Servlet servlet) { + this ( new LinkedList (), servlet ); + } + + public Builder(List connectors, Servlet servlet) { + if (connectors == null) { + throw new IllegalArgumentException("You must provide a connector list"); + } + + this.connectors = connectors; + this.servlet = servlet; + } + + public Builder name(String name) { this.name = name; return this; } + public Builder encodeSlashes(boolean encodeSlashes) { this.encodeSlashes = encodeSlashes; return this; } + public Builder withConnector ( ApiServerConnector c ) { this.connectors.add ( c ); return this; } + public Builder usingProtocol ( String cn ) { this.protocolClassName = cn; return this; } + + public String name() { return name; } + public List connectors() { return connectors; } + public Servlet servlet() { return servlet; } + public boolean encodeSlashes() { return encodeSlashes; } + + public ApiServer build() throws IOException { + if ( connectors.size () < 1 ) throw new IllegalArgumentException("At least one connector must be provided to build an API Server"); + return new ApiServer(this); + } + } + + public void await() { + tomcat.getServer().await(); + } + + public void start() throws LifecycleException, IOException { + + tomcat.start(); + + try { + waitForTomcatLifecycleState(LifecycleState.STARTED); + } catch(InterruptedException e) { + //Ignore + } + } + + public void stop() throws LifecycleException { + + tomcat.stop(); + + try { + waitForTomcatLifecycleState(LifecycleState.STOPPED); + } catch(InterruptedException e) { + //Ignore + } + + destroy(); + } + + public void destroy() throws LifecycleException { + tomcat.destroy(); + + try { + waitForTomcatLifecycleState(LifecycleState.DESTROYED); + } catch(InterruptedException e) { + //Ignore + } + } + + public static void removeLeadingDashes ( Map argMap ) + { + final HashMap replace = new HashMap (); + for ( Entry e : argMap.entrySet () ) + { + replace.put ( + e.getKey().startsWith("-")? e.getKey().substring(1) : e.getKey(), + e.getValue() ); + } + argMap.clear (); + argMap.putAll ( replace ); + } + + private void waitForTomcatLifecycleState(LifecycleState s) throws InterruptedException { + final Object mutex = new Object(); + final LifecycleState state = s; + + tomcat.getServer().addLifecycleListener(new LifecycleListener() { + @Override + public void lifecycleEvent(LifecycleEvent event) { + if (event.getLifecycle().getState() == state) { + synchronized (mutex) { + tomcat.getServer().removeLifecycleListener(this); + mutex.notify(); + } + } + } + }); + + synchronized (mutex) { + while (tomcat.getServer().getState() != state) + mutex.wait(); + } + } + + /** + * make a tmp dir using the temp file facility to create a file, then replace it with a dir + * @param name + * @return + * @throws IOException + */ + private File makeTmpDir ( String name ) throws IOException + { + final File temp = File.createTempFile (name + ".", Long.toString ( System.nanoTime() ) ); + + if ( !temp.delete () ) + { + throw new IOException ( "Couldn't delete tmp file " + temp.getAbsolutePath () ); + } + if ( !temp.mkdir () ) + { + throw new IOException ( "Couldn't create tmp dir " + temp.getAbsolutePath () ); + } + temp.deleteOnExit (); + return temp; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServerConnector.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServerConnector.java new file mode 100644 index 0000000..ab8f797 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/ApiServerConnector.java @@ -0,0 +1,123 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +public class ApiServerConnector { + + private final boolean isSecure; + private final int port; + private final int maxThreads; + private final String keystorePassword; + private final String keystoreFile; + private final String keyAlias; + private final int socketTimeoutMs; + private final int keepAliveTimeoutMs; + + public static class Builder { + + private final int port; + + private boolean secure = false; + private String keystorePassword = null; + private String keystoreFile = null; + private String keyAlias = null; + private int maxThreads = -1; + private int socketTimeoutMs = 1000 * 5; + private int keepAliveTimeoutMs = 1000 * 60 * 2; + + public Builder(int port) { + this.port = port; + } + + public ApiServerConnector build() { + + if (secure) { + if (keystoreFile == null || keystoreFile.length() == 0) { + throw new IllegalArgumentException("Secure port " + port + " was specified, but no keystoreFile. Fix secure service configuration"); + } else if (keystorePassword == null || keystorePassword.length() == 0) { + throw new IllegalArgumentException("Secure port " + port + " was specified, but no keystorePassword. Fix secure service configuration"); + } + } + + return new ApiServerConnector(this); + } + + public Builder secure(boolean secure) { + this.secure = secure; + return this; + } + + public Builder maxThreads(int maxThreads) { + this.maxThreads = maxThreads; + return this; + } + + public Builder keystorePassword(String keystorePassword) { + this.keystorePassword = keystorePassword; + return this; + } + + public Builder keystoreFile(String keystoreFile) { + this.keystoreFile = keystoreFile; + return this; + } + + public Builder keyAlias(String keyAlias) { + this.keyAlias = keyAlias; + return this; + } + + // timeouts: The AT&T CSO team believes that a long time window between socket connect + // and byte send makes DoS attacks more effective. (If you're thinking "that doesn't + // make any sense", you're correct.) However, the vulnerability scanner they run flags + // tomcat sockets running at default timeouts causing email from people responsible + // for closing the vulnerability flag regardless of its making any sense. So, we + // default the connectionTimeout (time between connect and byte send) to 5 seconds unless + // someone sets it explicitly via builder. That same value is used for the keep alive + // timeout (HTTP 1.1) unless explicitly set, and 5 seconds is very short for a long-polling + // cambria client (for example), so we jack that back up to a fairly high value by default. + + public Builder connectionTimingOutAfter ( int timeoutMs ) + { + this.socketTimeoutMs = timeoutMs; + return this; + } + + public Builder keepAliveTimingOutAfter ( int timeoutMs ) + { + this.keepAliveTimeoutMs = timeoutMs; + return this; + } + + private boolean isSecure() { return secure; } + private int port() { return port; } + private String keystorePassword() { return keystorePassword; } + private String keystoreFile() { return keystoreFile; } + private String keyAlias() { return keyAlias; } + private int maxThreads() { return maxThreads; } + private int socketTimeoutMs() { return socketTimeoutMs; } + private int keepAliveTimeoutMs() { return keepAliveTimeoutMs; } + } + + private ApiServerConnector(Builder builder) { + this.isSecure = builder.isSecure(); + this.port = builder.port(); + this.keystorePassword = builder.keystorePassword(); + this.keystoreFile = builder.keystoreFile(); + this.keyAlias = builder.keyAlias(); + this.maxThreads = builder.maxThreads(); + this.socketTimeoutMs = builder.socketTimeoutMs (); + this.keepAliveTimeoutMs = builder.keepAliveTimeoutMs (); + } + + public boolean isSecure() { return isSecure; } + public int port() { return port; } + public int maxThreads() { return maxThreads; } + public String keystorePassword() { return keystorePassword; } + public String keystoreFile() { return keystoreFile; } + public String keyAlias() { return keyAlias; } + + public int socketTimeoutMs() { return socketTimeoutMs; } + public int keepAliveTimeoutMs() { return keepAliveTimeoutMs; } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/CommonServlet.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/CommonServlet.java new file mode 100644 index 0000000..f20b125 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/CommonServlet.java @@ -0,0 +1,436 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URL; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.Key; +import java.util.Arrays; +import java.util.UUID; + +import javax.servlet.ServletException; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.apiServer.util.Emailer; +import com.att.nsa.configs.ConfigDb; +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.confimpl.CassandraConfigDb; +import com.att.nsa.configs.confimpl.EncryptingLayer; +import com.att.nsa.configs.confimpl.FileSystemConfigDb; +import com.att.nsa.configs.confimpl.MemConfigDb; +import com.att.nsa.configs.confimpl.ZkConfigDb; +import com.att.nsa.drumlin.service.framework.DrumlinErrorHandler; +import com.att.nsa.drumlin.service.framework.DrumlinServlet; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.service.framework.routing.playish.DrumlinPlayishRoutingFileSource; +import com.att.nsa.drumlin.service.standards.HttpStatusCodes; +import com.att.nsa.drumlin.service.standards.MimeTypes; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.rrNvReadable.loadException; +import com.att.nsa.drumlin.till.nv.rrNvReadable.missingReqdSetting; +import com.att.nsa.logging.LoggingContext; +import com.att.nsa.security.NsaAuthenticatorService; +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; +import com.att.nsa.security.authenticators.OriginalUebAuthenticator; +import com.att.nsa.security.authenticators.RemoteSaIamAuthenticator; +import com.att.nsa.security.authenticators.SimpleAuthenticator; +import com.att.nsa.security.db.BaseNsaApiDbImpl; +import com.att.nsa.security.db.EncryptingApiDbImpl; +import com.att.nsa.security.db.NsaApiDb; +import com.att.nsa.security.db.NsaApiDb.KeyExistsException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKeyFactory; +import com.att.nsa.security.db.simple.NsaSimpleRemoteApiKey; +import com.att.nsa.util.rrConvertor; + +public abstract class CommonServlet extends DrumlinServlet +{ + // authentication system settings and defaults + public static final String kSetting_RequireSecureChannel = "authentication.requireSecureChannel"; + public static final boolean kDefault_RequireSecureChannel = true; + public static final String kSetting_RequestTimeWindow = "authentication.allowedTimeSkewMs"; + public static final long kDefault_RequestTimeWindow = 1000 * 60 * 10; // 10 minutes + + // admin API calls are protected via special API key "admin" and an API secret loaded from config + public static final String kSetting_AdminSecret = "authentication.adminSecret"; + + public static final String kSetting_UseLocalConfigDbAuth = "iam.local"; + public static final boolean kDefault_UseLocalConfigDbAuth = true; + + public static final String kSetting_UseRemoteAuth = "iam.remote"; + public static final boolean kDefault_UseRemoteAuth = false; + + // zookeeper related settings and defaults + public static final String kSetting_ZkConfigDbServers = "config.zk.servers"; + public static final String kSetting_ZkConfigDbRoot = "config.zk.root"; + public static final String kDefault_ZkConfigDbServers = "localhost:2181"; + + // file system related settings and defaults + public static final String kSetting_FsConfigDbDir = "config.fs.location"; + public static final String kDefault_FsConfigDbDir = "./etc/configdb"; + + // cassandra related settings and defaults + public static final String kSetting_CassandraContactPoint = "flatiron.cassandra.contactpoint"; + public static final String kSetting_CassandraPort = "flatiron.cassandra.port"; + public static final String kDefault_CassandraContactPoint = "localhost"; + public static final int kDefault_CassandraPort = 9042; + + public enum ConfigDbType + { + MEMORY, + FILESYSTEM, + ZOOKEEPER, + CASSANDRA + } + + public CommonServlet ( rrNvReadable settings, String sysName, boolean withApiKeyEndpoints ) throws loadException, missingReqdSetting + { + super ( settings, null, sessionLifeCycle.NO_SESSION ); + + fSysName = sysName; + fInited = false; + fWithApiKeyEndpoints = withApiKeyEndpoints; + fAdminUserKey = null; + } + + public ConfigDb getConfigDb () + { + checkInit (); + return fConfigDb; + } + + public NsaAuthenticatorService getSecurityManager () + { + checkInit (); + return fSecurityManager; + } + + public NsaApiDb getApiKeyDb () + { + checkInit (); + return fApiKeyDb; + } + + public Emailer getSystemEmailer () + { + checkInit (); + return fEmailer; + } + + /** + * check if this system has an admin user + * @return true if this system has an admin user + */ + public boolean hasAdminUser () + { + return fAdminUserKey != null; + } + + /** + * Check if the given user api key is the admin user. + * @param apiKey + * @return true if the api key is that of the admin user. + */ + public boolean isAdminUser ( String apiKey ) + { + return fAdminUserKey != null && fAdminUserKey.equals ( apiKey ); + } + + /** + * Get the default root node string for the ZK-backed config db + * @param sysName + * @return + */ + public static String getDefaultZkRoot ( String sysName ) + { + return "/fe3c/" + sysName + "/config"; + } + + /** + * Call this early in your servletSetup() to initialize services provided by this class. + * + * @param cdbType + * @throws rrNvReadable.missingReqdSetting + * @throws rrNvReadable.invalidSettingValue + * @throws ServletException + * @throws ConfigDbException + * @throws IOException + */ + protected synchronized void commonServletSetup ( ConfigDbType cdbType ) throws ConfigDbException, missingReqdSetting, IOException + { + if ( fInited ) + { + log.warn ( "CommonServlet is already initialized." ); + } + fInited = true; + + // setup some constant log info (for ECOMP) + setupEcompLogging (); + + // setup standard error handling replies + setupStandardErrorHandlers (); + + // build a configdb + final rrNvReadable settings = getSettings (); + if ( cdbType == null || cdbType == ConfigDbType.MEMORY ) + { + fConfigDb = new MemConfigDb (); + } + else if ( cdbType == ConfigDbType.FILESYSTEM ) + { + final File baseDir = new File ( settings.getString ( kSetting_FsConfigDbDir, kDefault_FsConfigDbDir ) ); + fConfigDb = new FileSystemConfigDb ( baseDir ); + } + else if ( cdbType == ConfigDbType.ZOOKEEPER ) + { + final String defRoot = getDefaultZkRoot ( fSysName ); + fConfigDb = new ZkConfigDb ( + settings.getString ( kSetting_ZkConfigDbServers, kDefault_ZkConfigDbServers ), + settings.getString ( kSetting_ZkConfigDbRoot, defRoot ) + ); + } + else if ( cdbType == ConfigDbType.CASSANDRA ) + { + final int cassandraPort = getSettings ().getInt ( kSetting_CassandraPort, kDefault_CassandraPort ); + final String contactPoints = getSettings ().getString ( kSetting_CassandraContactPoint, kDefault_CassandraContactPoint ); + fConfigDb = new CassandraConfigDb ( Arrays.asList ( contactPoints.split ( "," ) ), cassandraPort ); + } + else + { + throw new IllegalArgumentException ( "Unrecognized configDb type: " + cdbType ); + } + + // setup authentication system + fApiKeyDb = buildApiKeyDb ( settings, fConfigDb ); + fSecurityManager = new NsaAuthenticatorService ( + settings.getBoolean ( kSetting_RequireSecureChannel, kDefault_RequireSecureChannel ) + ); + + // add the usual api key authenticator + if ( settings.getBoolean ( kSetting_UseLocalConfigDbAuth, kDefault_UseLocalConfigDbAuth ) ) + { + fSecurityManager.addAuthenticator ( new OriginalUebAuthenticator ( + fApiKeyDb, + settings.getLong ( kSetting_RequestTimeWindow, kDefault_RequestTimeWindow ) ) + ); + } + + // add the remote api key authenticator + if ( settings.getBoolean ( kSetting_UseRemoteAuth, kDefault_UseRemoteAuth ) ) + { + try + { + fSecurityManager.addAuthenticator ( new RemoteSaIamAuthenticator ( settings, new RemoteSaIamAuthenticator.ApiKeyFactory() + { + @Override + public NsaSimpleApiKey createApiKey ( JSONObject data ) + { + return new NsaSimpleRemoteApiKey ( data ); + } + } ) ); + } + catch ( GeneralSecurityException e ) + { + log.warn ( "Couldn't create a remote authenticator client: " + e.getMessage(), e ); + } + } + + // add the admin authenticator + final String adminSecret = settings.getString ( kSetting_AdminSecret, null ); + if ( adminSecret != null && adminSecret.length () > 0 ) + { + fAdminUserKey = "admin"; + try + { + // add via API key auth + final NsaApiDb adminDb = new BaseNsaApiDbImpl ( new MemConfigDb(), new NsaSimpleApiKeyFactory() ); + adminDb.createApiKey ( fAdminUserKey, adminSecret ); + fSecurityManager.addAuthenticator ( new OriginalUebAuthenticator ( adminDb, 10*60*1000 ) ); + + // also add admin via Basic Auth + fSecurityManager.addAuthenticator ( new SimpleAuthenticator ().add ( "admin", adminSecret ) ); + } + catch ( KeyExistsException e ) + { + throw new RuntimeException ( "This key can't exist in a fresh in-memory DB!", e ); + } + } + + // setup the emailer + fEmailer = new Emailer ( settings ); + + // common endpoints + if ( fWithApiKeyEndpoints ) + { + final URL routes = findStream ( "iamRoutes.conf" ); + if ( routes == null ) + { + log.warn ( "System requested IAM routes, but iamRoutes.conf was not found." ); + } + else + { + final DrumlinPlayishRoutingFileSource drs = new DrumlinPlayishRoutingFileSource ( routes ); + getRequestRouter ().addRouteSource ( drs ); + } + } + } + + private NsaApiDb buildApiKeyDb ( rrNvReadable settings, ConfigDb cdb ) throws ConfigDbException, missingReqdSetting + { + // this system uses an encrypted api key db + + final String keyBase64 = settings.getString ( fSysName + ".secureConfig.key", null ); + final String initVectorBase64 = settings.getString ( fSysName + ".secureConfig.iv", null ); + + // if neither value was provided, don't encrypt api key db + if ( keyBase64 == null && initVectorBase64 == null ) + { + log.warn ( "This server is configured to use an unencrypted API key database. See the settings documentation." ); + return new BaseNsaApiDbImpl ( cdb, new NsaSimpleApiKeyFactory () ); + } + else if ( keyBase64 == null ) + { + // neither or both, otherwise something's goofed + throw new missingReqdSetting ( fSysName + ".secureConfig.key" ); + } + else if ( initVectorBase64 == null ) + { + // neither or both, otherwise something's goofed + throw new missingReqdSetting ( fSysName + ".secureConfig.iv" ); + } + else + { + log.info ( "This server is configured to use an encrypted API key database." ); + final Key key = EncryptingLayer.readSecretKey ( keyBase64 ); + final byte[] iv = rrConvertor.base64Decode ( initVectorBase64 ); + return new EncryptingApiDbImpl ( cdb, new NsaSimpleApiKeyFactory (), key, iv ); + } + } + + private final String fSysName; + private final boolean fWithApiKeyEndpoints; + private boolean fInited; + private ConfigDb fConfigDb; + private NsaApiDb fApiKeyDb; + private NsaAuthenticatorService fSecurityManager; + private Emailer fEmailer; + private String fAdminUserKey; + + private static final long serialVersionUID = 1L; + + /* + * Our servers are meant as the front-end to stand-alone services, but because they're used + * by various ECOMP clients, and may be bundled in distribution with them someday, the GFP + * SE group things that our servers must comply with general ECOMP requirements. This + * code sets up a number of static fields that are required by ECOMP's (very amateur) logging + * standard. The fields populated here are available via Log4J's EnhancedPatternLayout + * system, using "%X{key}" to get the value of "key". + * + * As of 7 July 2015, the logging standard is available on Rally: + * https://rally1.rallydev.com/slm/attachment/38451080500/ECOMP%20platform%20application%20logging%20guidelines.docx + * + * A log format line that meets current reqs looks like: + * + * %d{yyyy-MM-dd'T'HH:mm:ss}{GMT+0}+00:00|%X{requestId}|%X{serviceInstanceId}|%-10t|%X{serverName}|%X{serviceName}|%X{instanceUuid}|%-5p|%X{severity}|%X{serverIpAddress}|%X{server}|%X{ipAddress}|%X{className}|%X{timer}|%m%n + * + * Each field referenced inside a %X{} should be available to fully meet the reqs. + */ + private void setupEcompLogging () + { + final LoggingContext lc = getCommonLoggingContext (); + + String ipAddr = "127.0.0.1"; + String hostname = "localhost"; + try + { + final InetAddress ip = InetAddress.getLocalHost (); + hostname = ip.getCanonicalHostName (); + ipAddr = ip.getHostAddress(); + } + catch ( UnknownHostException x ) + { + // just use localhost + } + + lc.put ( "serverName", hostname ); + lc.put ( "serviceName", fSysName ); + lc.put ( "server", hostname ); + lc.put ( "serverIpAddress", ipAddr.toString () ); + + // instance UUID is meaningless here, so we just create a new one each time the + // server starts. One could argue each new instantiation of the service should + // have a new instance ID. + lc.put ( "instanceUuid", UUID.randomUUID ().toString () ); + + // *really* meaningless data + lc.put ( "severity", "" ); + } + + private void setupStandardErrorHandlers () + { + final DrumlinRequestRouter drr = getRequestRouter (); + + drr.setHandlerForException ( AccessDeniedException.class, new DrumlinErrorHandler() + { + @Override + public void handle ( DrumlinRequestContext ctx, Throwable cause ) + { + sendJsonReply ( ctx, + HttpStatusCodes.k401_unauthorized, + "Access denied. Check your API key and signature, or check with the scope administrator." + ); + } + }); + + drr.setHandlerForException ( ConfigDbException.class, new DrumlinErrorHandler() + { + @Override + public void handle ( DrumlinRequestContext ctx, Throwable cause ) + { + sendJsonReply ( ctx, + HttpStatusCodes.k503_serviceUnavailable, + "There was a problem using the cluster's configuration database. Contact the cluster administrators or try again later." ); + } + }); + + drr.setHandlerForException ( JSONException.class, new DrumlinErrorHandler() + { + @Override + public void handle ( DrumlinRequestContext ctx, Throwable cause ) + { + sendJsonReply ( ctx, + HttpStatusCodes.k400_badRequest, + "There's a problem with your JSON. " + cause.getMessage () + ); + } + }); + } + + protected void sendJsonReply ( DrumlinRequestContext ctx, int statusCode, String errMsg ) + { + ctx.response ().sendErrorAndBody ( + statusCode, + new JSONObject() + .put ( "statusCode", statusCode ) + .put ( "error", errMsg ) + .toString(), + MimeTypes.kAppJson ); + } + + private void checkInit () + { + if ( !fInited ) throw new IllegalStateException ( "CommonServlet was not initialzied." ); + } + + private static final Logger log = LoggerFactory.getLogger ( CommonServlet.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/NsaAppException.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/NsaAppException.java new file mode 100644 index 0000000..d252c23 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/NsaAppException.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONObject; + +import com.att.nsa.drumlin.service.standards.MimeTypes; + +public class NsaAppException extends Exception +{ + public NsaAppException ( JSONObject jsonObject ) + { + this ( HttpServletResponse.SC_OK, jsonObject ); + } + + public NsaAppException ( int status, String msg ) + { + this ( status, makeObject ( status, msg ) ); + } + + public NsaAppException ( int status, JSONObject jsonObject ) + { + super ( "" + status + " " + jsonObject.toString () ); + + fStatus = status; + fBody = jsonObject; + fType = MimeTypes.kAppJson; + } + + public int getStatus () + { + return fStatus; + } + + public String getMediaType () + { + return fType; + } + + public String getBody () + { + return fBody.toString (); + } + + private static final long serialVersionUID = 1L; + + private static JSONObject makeObject ( int status, String msg ) + { + final JSONObject o = new JSONObject (); + o.put ( "message", msg ); + return o; + } + + private final String fType; + private final JSONObject fBody; + private final int fStatus; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/Settings.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/Settings.java new file mode 100644 index 0000000..116c3d7 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/Settings.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * A general purpose settings class. + * @author peter + * + */ +public interface Settings extends rrNvReadable +{ +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaApiKeyEndpoint.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaApiKeyEndpoint.java new file mode 100644 index 0000000..c3c6c5f --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaApiKeyEndpoint.java @@ -0,0 +1,233 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.endpoints; + +import java.io.IOException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import com.att.nsa.apiServer.util.Emailer; +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.standards.HttpStatusCodes; +import com.att.nsa.drumlin.till.data.uniqueStringGenerator; +import com.att.nsa.drumlin.util.JsonBodyReader; +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; +import com.att.nsa.security.db.NsaApiDb; +import com.att.nsa.security.db.NsaApiDb.KeyExistsException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; + +public class NsaApiKeyEndpoint extends NsaBaseEndpoint +{ + /** + * Get all the API keys, with some descriptive information. This is an open + * query, so we don't want to expose anything confidential (e.g. the secret key) + * @param ctx + * @throws IOException + * @throws AccessDeniedException + */ + public static void getAllApiKeys(DrumlinRequestContext ctx) throws IOException, ConfigDbException, AccessDeniedException + { + // there's no reason for anyone but an admin to do this. It's also potentially + // taxing on the back-end system (e.g. ZK) + NsaBaseEndpoint.adminAuthenticate ( ctx ); + + final NsaApiDb apiDb = getApiKeyDb ( ctx ); + + final JSONObject result = new JSONObject (); + final JSONArray keys = new JSONArray (); + result.put ( "apiKeys", keys ); + + for ( String key : apiDb.loadAllKeys () ) + { + keys.put ( key ); + } + + respondOk ( ctx, result ); + } + + public static void getApiKey ( DrumlinRequestContext ctx, String apiKeyName ) throws IOException, ConfigDbException + { + final NsaApiDb apiDb = getApiKeyDb ( ctx ); + + final NsaSimpleApiKey key = apiDb.loadApiKey ( apiKeyName ); + if ( key == null ) + { + respondWithErrorInJson ( ctx, HttpStatusCodes.k404_notFound, "No API key " + apiKeyName + "." ); + return; + } + + respondOk ( ctx, key.asJsonObject () ); + } + + private static final String kSetting_AllowAnonymousKeys = "apiKeys.allowAnonymous"; + + public static void createApiKey ( DrumlinRequestContext ctx ) throws IOException, AccessDeniedException, ConfigDbException + { + try + { + final JSONObject dataIn = readJsonBody ( ctx ); + final String contactEmail = dataIn.optString ( "email" ); + final String description = dataIn.optString ( "description" ); + + final boolean emailProvided = contactEmail != null && contactEmail.length() > 0; + if ( !ctx.getServlet ().getSettings ().getBoolean ( kSetting_AllowAnonymousKeys, false ) && + !emailProvided ) + { + respondWithErrorInJson ( ctx, HttpStatusCodes.k400_badRequest, "You must provide an email address." ); + return; + } + + final String keyString = generateKey ( 16 ); + final String sharedSecret = generateKey ( 24 ); + + try + { + final NsaApiDb apiDb = getApiKeyDb ( ctx ); + final NsaSimpleApiKey key = apiDb.createApiKey ( keyString, sharedSecret ); + + if ( contactEmail != null ) key.setContactEmail ( contactEmail ); + if ( description != null ) key.setDescription ( description ); + apiDb.saveApiKey ( key ); + + // email out the secret to validate the email address + if ( emailProvided ) + { + final String body = new StringBuilder () + .append ( "\n" ) + .append ( "Your email address was provided as the creator of new API key \"" ) + .append ( keyString ) + .append ( "\".\n" ) + .append ( "\n" ) + .append ( "If you did not make this request, please let us know. See http://sa2020.it.att.com:8888 for contact information, " ) + .append ( "but don't worry - the API key is useless without the information below, which has been provided only to you.\n" ) + .append ( "\n\n" ) + .append ( "For API key \"" ) + .append ( keyString ) + .append ( "\", use API key secret:\n\n\t" ) + .append ( sharedSecret ) + .append ( "\n\n" ) + .append ( "Note that it's normal to share the API key (" ) + .append ( keyString ) + .append ( "). This is how you are granted access to resources " ) + .append ( "like a UEB topic or Flatiron scope. However, you should NOT share the API key's secret. " ) + .append ( "The API key is associated with your email alone. ALL access to data made with this " ) + .append ( "key will be your responsibility. If you share the secret, someone else can use the API key " ) + .append ( "to access proprietary data with your identity.\n" ) + .append ( "\n" ) + .append ( "Enjoy!\n" ) + .append ( "\n" ) + .append ( "The GFP/SA-2020 Team" ) + .toString (); + + final Emailer em = getServlet ( ctx ).getSystemEmailer (); + em.send ( contactEmail, "New API Key", body ); + } + + final JSONObject o = key.asJsonObject (); + o.put ( NsaSimpleApiKey.kApiSecretField, + emailProvided ? + "Emailed to " + contactEmail + "." : + key.getSecret () + ); + respondOk ( ctx, o ); + } + catch ( KeyExistsException e ) + { + // holy smokes. go play the lottery. + respondWithErrorInJson ( ctx, HttpStatusCodes.k500_internalServerError, "Randomly created API key conflicts with existing key." ); + } + } + catch ( JSONException x ) + { + respondWithErrorInJson ( ctx, HttpStatusCodes.k400_badRequest, "Couldn't parse your JSON" ); + } + } + + public static void updateApiKey ( DrumlinRequestContext ctx, String apiKeyName ) throws IOException, AccessDeniedException, ConfigDbException + { + final NsaApiDb apiDb = getApiKeyDb ( ctx ); + + final NsaSimpleApiKey key = apiDb.loadApiKey ( apiKeyName ); + if ( key == null ) + { + respondWithErrorInJson ( ctx, HttpStatusCodes.k404_notFound, "No API key named " + apiKeyName ); + return; + } + + // update the existing api key if allowed + final NsaSimpleApiKey user = getAuthenticatedUser ( ctx ); + if ( user == null || !user.getKey().equals ( key.getKey () ) ) + { + throw new AccessDeniedException ( "You must authenticate with the key you'd like to update." ); + } + + // this user is okay to update the key. get the posted content. + final JSONObject dataIn = JsonBodyReader.readBody ( ctx.request () ); + + boolean updates = false; + + // NOTE! We cannot allow an email update to an API key, because the email address is + // validated at creation time and actions taken with the API key are associated + // with the email address. + + final String description = dataIn.optString ( "description" ); + if ( description != null ) + { + key.setDescription ( description ); + updates = true; + } + + // other updates? perhaps last ack time? + + if ( updates ) + { + apiDb.saveApiKey ( key ); + } + + respondOkNoContent ( ctx ); + } + + public static void deleteApiKey ( DrumlinRequestContext ctx, String apiKeyName ) throws IOException, AccessDeniedException, ConfigDbException + { + final NsaApiDb apiDb = getApiKeyDb ( ctx ); + + final NsaSimpleApiKey key = apiDb.loadApiKey ( apiKeyName ); + if ( key == null ) + { + respondWithErrorInJson ( ctx, HttpStatusCodes.k404_notFound, "No API key named " + apiKeyName ); + return; + } + + // delete the existing api key if allowed + final NsaSimpleApiKey user = getAuthenticatedUser ( ctx ); + if ( user == null || !user.getKey().equals ( key.getKey () ) ) + { + throw new AccessDeniedException ( "You don't own the API key." ); + } + + // delete it + apiDb.deleteApiKey ( key ); + + respondOkNoContent ( ctx ); + } + + private static String kKeyChars = "ABCDEFGHJIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static String generateKey ( int length ) + { + return uniqueStringGenerator.createKeyUsingAlphabet ( kKeyChars, length ); + } + + /** + * Get the API db + * @param ctx + * @return + */ + public static NsaApiDb getApiKeyDb ( DrumlinRequestContext ctx ) + { + return getServlet(ctx).getApiKeyDb (); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaBaseEndpoint.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaBaseEndpoint.java new file mode 100644 index 0000000..2f778ae --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/endpoints/NsaBaseEndpoint.java @@ -0,0 +1,259 @@ +package com.att.nsa.apiServer.endpoints; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletResponse; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +import com.att.nsa.apiServer.CommonServlet; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.context.DrumlinResponse; +import com.att.nsa.drumlin.service.standards.MimeTypes; +import com.att.nsa.security.NsaAuthenticatorService; +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; + +/** + * Convenience base class for endpoint classes. + * @author peter + * + * @param + */ +public class NsaBaseEndpoint +{ + protected static final int kBufferLength = 4096; + + public interface StreamWriter + { + void write ( OutputStream os ) throws IOException; + } + + /** + * Non-idempotent GET requests should have no-cache flags in the response header + * @param ctx + */ + public static void setNoCacheHeadings ( DrumlinRequestContext ctx ) + { + final DrumlinResponse r = ctx.response (); + r.writeHeader ( "Cache-Control", "no-store, no-cache, must-revalidate" ); + r.writeHeader ( "Pragma", "no-cache" ); + r.writeHeader ( "Expires", "0" ); + } + + /** + * Send a success response with content. + * @param ctx + * @param result + * @throws JSONException + * @throws IOException + */ + public static void respondOk ( DrumlinRequestContext ctx, JSONObject result ) throws IOException + { + respondOkWithStream ( ctx, "application/json", new ByteArrayInputStream(result.toString(4).getBytes ()) ); + } + + /** + * Send a success response without content. + * @param ctx + * @param result + * @throws JSONException + * @throws IOException + */ + public static void respondOkNoContent ( DrumlinRequestContext ctx ) throws IOException + { + ctx.response ().setStatus ( HttpServletResponse.SC_NO_CONTENT ); + } + + /** + * Send a success response with content. + * @param ctx + * @param result + * @throws JSONException + * @throws IOException + */ + public static void respondOkWithHtml ( DrumlinRequestContext ctx, String html ) throws IOException + { + respondOkWithStream ( ctx, "text/html", new ByteArrayInputStream(html.toString().getBytes ()) ); + } + + /** + * Send a success response with a content stream. + * @param ctx + * @param mediaType + * @param is + * @throws IOException + */ + public static void respondOkWithStream ( DrumlinRequestContext ctx, String mediaType, final InputStream is ) throws IOException + { + respondOkWithStream ( ctx, mediaType, new StreamWriter () + { + @Override + public void write ( OutputStream os ) throws IOException + { + copyStream ( is, os ); + } + }); + } + + /** + * Send a success response with a content stream. + * @param ctx + * @param mediaType + * @param is + * @throws IOException + */ + public static void respondOkWithStream ( DrumlinRequestContext ctx, String mediaType, final StreamWriter writer ) throws IOException + { + ctx.response ().setStatus ( HttpServletResponse.SC_OK ); + final OutputStream os = ctx.response ().getStreamForBinaryResponse ( mediaType ); + writer.write ( os ); + } + + /** + * Respond to the client with the given error code and status message + * @param ctx + * @param errCode + * @param msg + * @throws IOException + */ + public static void respondWithError ( DrumlinRequestContext ctx, int errCode, String msg ) throws IOException + { + ctx.response ().sendError ( errCode, msg ); + } + + /** + * Respond to the client with the given error code and status message and a JSON body + * @param ctx + * @param errCode + * @param msg + * @throws IOException + */ + public static void respondWithErrorInJson ( DrumlinRequestContext ctx, int errCode, String msg ) throws IOException + { + final JSONObject o = new JSONObject (); + o.put ( "status", errCode ); + o.put ( "message", msg ); + respondWithError ( ctx, errCode, o ); + } + + /** + * Respond to the client with the given error code and status message + * @param ctx + * @param errCode + * @param msg + * @throws IOException + */ + public static void respondWithError ( DrumlinRequestContext ctx, int errCode, JSONObject body ) throws IOException + { + ctx.response ().sendErrorAndBody ( errCode, body.toString ( 4 ), MimeTypes.kAppJson ); + } + + /** + * Copy from the input stream to the output stream, then close the output stream. + * @param in + * @param out + * @throws IOException + */ + public static void copyStream ( InputStream in, OutputStream out ) throws IOException + { + copyStream ( in, out, kBufferLength ); + } + + /** + * Copy from the input stream to the output stream, then close the output stream. + * @param in + * @param out + * @param bufferSize + * @throws IOException + */ + public static void copyStream ( InputStream in, OutputStream out, int bufferSize ) throws IOException + { + final byte[] buffer = new byte [bufferSize]; + int len; + while ( ( len = in.read ( buffer ) ) != -1 ) + { + out.write ( buffer, 0, len ); + } + out.close (); + } + + /** + * Get the typed servlet (this will throw ClassCastException if it's not the + * correct servlet type!) + * @param ctx + * @return + */ + public static CommonServlet getServlet ( DrumlinRequestContext ctx ) + { + return (CommonServlet) ctx.getServlet (); + } + + /** + * Get the user associated with the incoming request, or null if the user is not authenticated. + * @param ctx + * @return + */ + public static NsaSimpleApiKey getAuthenticatedUser ( DrumlinRequestContext ctx ) + { + final CommonServlet s = getServlet ( ctx ); + final NsaAuthenticatorService m = s.getSecurityManager (); + return m.authenticate ( ctx ); + } + + /** + * Authenticate the caller as the system administrator + * @param ctx + * @throws AccessDeniedException + */ + public static NsaSimpleApiKey adminAuthenticate ( DrumlinRequestContext ctx ) throws AccessDeniedException + { + final CommonServlet s = getServlet ( ctx ); + final NsaAuthenticatorService m = s.getSecurityManager (); + final NsaSimpleApiKey user = m.authenticate ( ctx ); + if ( user == null || !user.getKey ().equals ( "admin" ) ) + { + throw new AccessDeniedException (); + } + return user; + } + + /** + * Read a JSON object body + * @param ctx + * @return a JSON object + * @throws JSONException + * @throws IOException + */ + public static JSONObject readJsonBody ( DrumlinRequestContext ctx ) throws JSONException, IOException + { + return new JSONObject ( new JSONTokener ( ctx.request ().getBodyStream () ) ); + } + + public static void sendJson ( DrumlinResponse r, JSONObject json ) throws IOException + { + sendJson ( r, json.toString (4) ); + } + + public static void sendJson ( DrumlinResponse r, String json ) throws IOException + { + final PrintWriter pw = r.getStreamForTextResponse ( MimeTypes.kAppJson ); + pw.println ( json ); + } + + public static void sendError ( DrumlinResponse r, int statusCode, String msg ) throws IOException + { + final JSONObject o = new JSONObject () + .put ( "error", msg ) + .put ( "status", statusCode ) + ; + r.sendErrorAndBody ( statusCode, o.toString (), MimeTypes.kAppJson ); + } +} + diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/metrics/cambria/MetricsSender.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/metrics/cambria/MetricsSender.java new file mode 100644 index 0000000..85a8fd0 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/metrics/cambria/MetricsSender.java @@ -0,0 +1,149 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.metrics.cambria; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.cambria.client.CambriaClientBuilders; +import com.att.nsa.cambria.client.CambriaPublisher; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.metrics.CdmMetricsRegistry; + +/** + * MetricsSender will send the given metrics registry content as an event on the Cambria + * event broker to the given topic. + * + * @author peter + * + */ +public class MetricsSender implements Runnable +{ + public static final String kSetting_CambriaEnabled = "metrics.send.cambria.enabled"; + public static final String kSetting_CambriaBaseUrl = "metrics.send.cambria.baseUrl"; + public static final String kSetting_CambriaTopic = "metrics.send.cambria.topic"; + public static final String kSetting_CambriaSendFreqSecs = "metrics.send.cambria.sendEverySeconds"; + + /** + * Schedule a periodic send of the given metrics registry using the given settings + * container for the Cambria location, topic, and send frequency.
+ *
+ * If the enabled flag is false, this method returns null. + * + * @param scheduler + * @param metrics + * @param settings + * @return a handle to the scheduled task + */ + public static ScheduledFuture sendPeriodically ( ScheduledExecutorService scheduler, + CdmMetricsRegistry metrics, rrNvReadable settings, String defaultTopic ) + { + if ( settings.getBoolean ( kSetting_CambriaEnabled, true ) ) + { + return MetricsSender.sendPeriodically ( scheduler, metrics, + settings.getString ( kSetting_CambriaBaseUrl, "localhost" ), + settings.getString ( kSetting_CambriaTopic, defaultTopic ), + settings.getInt ( kSetting_CambriaSendFreqSecs, 30 ) ); + } + else + { + return null; + } + } + + /** + * Schedule a periodic send of the metrics registry to the given Cambria broker + * and topic. + * + * @param scheduler + * @param metrics the registry to send + * @param cambriaBaseUrl the base URL for Cambria + * @param topic the topic to publish on + * @param everySeconds how frequently to publish + * @return a handle to the scheduled task + */ + public static ScheduledFuture sendPeriodically ( ScheduledExecutorService scheduler, + CdmMetricsRegistry metrics, String cambriaBaseUrl, String topic, int everySeconds ) + { + return scheduler.scheduleAtFixedRate ( + new MetricsSender ( metrics, cambriaBaseUrl, topic ), + everySeconds, everySeconds, TimeUnit.SECONDS ); + } + + /** + * Create a metrics sender. + * + * @param metrics + * @param cambriaBaseUrl + * @param topic + */ + public MetricsSender ( CdmMetricsRegistry metrics, String cambriaBaseUrl, String topic ) + { + try + { + fMetrics = metrics; + fHostname = InetAddress.getLocalHost ().getHostName (); + + // setup a publisher that will send metrics immediately + fCambria = new CambriaClientBuilders.PublisherBuilder () + .usingHosts ( cambriaBaseUrl ) + .onTopic ( topic ) + .limitBatch ( 1, 100 ) + .build (); + } + catch ( UnknownHostException | MalformedURLException | GeneralSecurityException e ) + { + log.warn ( "Unable to get localhost address in MetricsSender constructor.", e ); + throw new RuntimeException ( e ); + } + } + + /** + * Send on demand. + */ + public void send () + { + try + { + final JSONObject o = fMetrics.toJson (); + o.put ( "hostname", fHostname ); + o.put ( "now", System.currentTimeMillis () ); + fCambria.send ( fHostname, o.toString () ); + } + catch ( JSONException e ) + { + log.warn ( "Error posting metrics to Cambria: " + e.getMessage () ); + } + catch ( IOException e ) + { + log.warn ( "Error posting metrics to Cambria: " + e.getMessage () ); + } + } + + /** + * Run() calls send(). It's meant for use in a background-scheduled task. + */ + @Override + public void run () + { + send (); + } + + private final CdmMetricsRegistry fMetrics; + private final CambriaPublisher fCambria; + private final String fHostname; + + private static final Logger log = LoggerFactory.getLogger ( MetricsSender.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/streams/ChunkedInputStream.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/streams/ChunkedInputStream.java new file mode 100644 index 0000000..f3034e4 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/streams/ChunkedInputStream.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.streams; + +import java.io.IOException; +import java.io.InputStream; + +public class ChunkedInputStream extends InputStream +{ + private static final int kCapacity = 1024 * 4; + + public ChunkedInputStream ( InputStream base ) + { + fBase = base; + fBuffer = new byte[kCapacity]; + fPos = 0; + fLen = 0; + fClosed = false; + fRemainingInChunk = 0; + } + + @Override + public void close () throws IOException + { + fBase.close (); + fClosed = true; + } + + @Override + public int read () throws IOException + { + while ( !fClosed ) + { + readMore (); + if ( fLen > 0 ) + { + int result = fBuffer[fPos++]; + fLen--; + return result; + } + } + return -1; + } + + private final InputStream fBase; + private final byte[] fBuffer; + private boolean fClosed; + private int fPos; + private int fLen; + private int fRemainingInChunk; + + private int readWithBlock ( byte[] bytes, int pos, int len ) throws IOException + { + int total = 0; + while ( total < len ) + { + final int now = fBase.read ( bytes, pos + total, len - total ); + if ( now < 0 ) + { + close (); + return -1; + } + + total += now; + } + return total; + } + + private void readMore () throws IOException + { + if ( fLen == 0 ) + { + if ( fRemainingInChunk == 0 ) + { + // start a new chunk + final int chunkLen = readChunkLength ( fBase ); + if ( chunkLen > 0 ) + { + fRemainingInChunk = chunkLen; + } + else if ( chunkLen == kClosed ) + { + close (); + } + else if ( chunkLen == 0 ) + { + // last chunk, read CRLF, + final byte[] bytes = new byte[2]; + if ( readWithBlock ( bytes, 0, 2 ) >= 0 ) + { + if ( bytes[0] != '\r' || bytes[1] != '\n' ) + { + throw new IOException ( "Chunked encoding format error." ); + } + } + fClosed = true; // there's no more here, but don't close the underlying stream + } + } + + if ( fRemainingInChunk > 0 ) + { + fLen = fBase.read ( fBuffer, 0, fRemainingInChunk ); + if ( fLen == -1 ) + { + close (); + return; + } + + fPos = 0; + fRemainingInChunk -= fLen; + + if ( fRemainingInChunk == 0 ) + { + // read trailing \r\n + final byte[] bytes = new byte[2]; + if ( readWithBlock ( bytes, 0, 2 ) >= 0 ) + { + if ( bytes[0] != '\r' || bytes[1] != '\n' ) + { + throw new IOException ( "Chunked encoding format error." ); + } + } + } + } + } + // else: wait until what's read is consumed + } + + private static int kClosed = -1; + private static int kNotAvailable = -2; + + private static int readChunkLength ( InputStream is ) throws IOException + { + if ( 0 == is.available () ) + { + return kNotAvailable; + } + + int result = 0; + + int c = is.read (); + while ( true ) + { + if ( c == -1 ) + { + return kClosed; + } + + int val = 0; + c = Character.toLowerCase ( c ); + if ( c >= '0' && c <= '9' ) + { + val = ( c - '0' ); + } + else if ( c >= 'a' && c <= 'f' ) + { + val = ( c - 'a' ) + 10; + } + else if ( c == '\r' ) + { + break; + } + else + { + throw new IOException ( "Chunked encoding format error." ); + } + + result = ( result * 16 ) + val; + c = is.read(); + } + + c = is.read (); + if ( c == -1 ) + { + return kClosed; + } + else if ( c == '\n' ) + { + // okay + } + else + { + // bogus + throw new IOException ( "Chunked encoding format error." ); + } + + return result; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/Emailer.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/Emailer.java new file mode 100644 index 0000000..299c4ce --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/Emailer.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.util; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.mail.BodyPart; +import javax.mail.Message; +import javax.mail.Multipart; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * Send an email from a message. + * + * @author peter + */ +public class Emailer +{ + public static final String kField_To = "to"; + public static final String kField_Subject = "subject"; + public static final String kField_Message = "message"; + + public Emailer ( rrNvReadable settings ) + { + fExec = Executors.newCachedThreadPool (); + fSettings = settings; + } + + public void send ( String to, String subj, String body ) throws IOException + { + final String[] addrs = to.split ( "," ); + + if ( to.length () > 0 ) + { + final MailTask mt = new MailTask ( addrs, subj, body ); + fExec.submit ( mt ); + } + else + { + log.warn ( "At least one address is required." ); + } + } + + public void close () + { + fExec.shutdown (); + } + + private final ExecutorService fExec; + private final rrNvReadable fSettings; + + private static final Logger log = LoggerFactory.getLogger ( Emailer.class ); + + public static final String kSetting_MailAuthUser = "mailLogin"; + public static final String kSetting_MailAuthPwd = "mailPassword"; + public static final String kSetting_MailFromEmail = "mailFromEmail"; + public static final String kSetting_MailFromName = "mailFromName"; + public static final String kSetting_SmtpServer = "mailSmtpServer"; + public static final String kSetting_SmtpServerPort = "mailSmtpServerPort"; + public static final String kSetting_SmtpServerSsl = "mailSmtpServerSsl"; + public static final String kSetting_SmtpServerUseAuth = "mailSmtpServerUseAuth"; + + private class MailTask implements Runnable + { + public MailTask ( String[] to, String subject, String msgBody ) + { + fToAddrs = to; + fSubject = subject; + fBody = msgBody; + } + + private String getSetting ( String settingKey, String defval ) + { + return fSettings.getString ( settingKey, defval ); + } + + // we need to get setting values from the evaluator but also the channel config + private void makeSetting ( Properties props, String propKey, String settingKey, String defval ) + { + props.put ( propKey, getSetting ( settingKey, defval ) ); + } + + private void makeSetting ( Properties props, String propKey, String settingKey, int defval ) + { + makeSetting ( props, propKey, settingKey, "" + defval ); + } + + private void makeSetting ( Properties props, String propKey, String settingKey, boolean defval ) + { + makeSetting ( props, propKey, settingKey, "" + defval ); + } + + @Override + public void run () + { + final StringBuffer tag = new StringBuffer (); + final StringBuffer addrList = new StringBuffer (); + tag.append ( "(" ); + for ( String to : fToAddrs ) + { + if ( addrList.length () > 0 ) + { + addrList.append ( ", " ); + } + addrList.append ( to ); + } + tag.append ( addrList.toString () ); + tag.append ( ") \"" ); + tag.append ( fSubject ); + tag.append ( "\"" ); + + log.info ( "sending mail to " + tag ); + + try + { + final Properties prop = new Properties (); + makeSetting ( prop, "mail.smtp.port", kSetting_SmtpServerPort, 587 ); + prop.put ( "mail.smtp.socketFactory.fallback", "false" ); + prop.put ( "mail.smtp.quitwait", "false" ); + makeSetting ( prop, "mail.smtp.host", kSetting_SmtpServer, "smtp.it.att.com" ); + makeSetting ( prop, "mail.smtp.auth", kSetting_SmtpServerUseAuth, true ); + makeSetting ( prop, "mail.smtp.starttls.enable", kSetting_SmtpServerSsl, true ); + + final String un = getSetting ( kSetting_MailAuthUser, "" ); + final String pw = getSetting ( kSetting_MailAuthPwd, "" ); + final Session session = Session.getInstance ( prop, + new javax.mail.Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication ( un, pw ); + } + } + ); + + final Message msg = new MimeMessage ( session ); + + final InternetAddress from = new InternetAddress ( + getSetting ( kSetting_MailFromEmail, "team@sa2020.it.att.com" ), + getSetting ( kSetting_MailFromName, "The GFP/SA2020 Team" ) ); + msg.setFrom ( from ); + msg.setReplyTo ( new InternetAddress[] { from } ); + msg.setSubject ( fSubject ); + + for ( String toAddr : fToAddrs ) + { + final InternetAddress to = new InternetAddress ( toAddr ); + msg.addRecipient ( Message.RecipientType.TO, to ); + } + + final Multipart multipart = new MimeMultipart ( "related" ); + final BodyPart htmlPart = new MimeBodyPart (); + htmlPart.setContent ( fBody, "text/plain" ); + multipart.addBodyPart ( htmlPart ); + msg.setContent ( multipart ); + + Transport.send ( msg ); + + log.info ( "mailing " + tag + " off without error" ); + } + catch ( Exception e ) + { + log.warn ( "Exception caught for " + tag, e ); + } + } + + private final String[] fToAddrs; + private final String fSubject; + private final String fBody; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/HumanReadableHelper.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/HumanReadableHelper.java new file mode 100644 index 0000000..4185e22 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/HumanReadableHelper.java @@ -0,0 +1,272 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class HumanReadableHelper +{ + private static final long kKilobyte = 1024; + private static final long kMegabyte = 1024 * kKilobyte; + private static final long kGigabyte = 1024 * kMegabyte; + private static final long kTerabyte = 1024 * kGigabyte; + private static final long kPetabyte = 1024 * kTerabyte; + private static final long kExabyte = 1024 * kPetabyte; + + public static String byteCountValue ( long inBytes ) + { + String result = "" + inBytes + " bytes"; + if ( inBytes > kExabyte ) + { + double d = inBytes / kExabyte; + result = "" + d + " EB"; + } + else if ( inBytes > kPetabyte ) + { + double d = inBytes / kPetabyte; + result = "" + d + " PB"; + } + else if ( inBytes > kTerabyte ) + { + double d = inBytes / kTerabyte; + result = "" + d + " TB"; + } + else if ( inBytes > kGigabyte ) + { + double d = inBytes / kGigabyte; + result = "" + d + " GB"; + } + else if ( inBytes > kMegabyte ) + { + double d = inBytes / kMegabyte; + result = "" + d + " MB"; + } + else if ( inBytes > kKilobyte ) + { + double d = inBytes / kKilobyte; + result = "" + d + " KB"; + } + return result; + } + + @Deprecated + public static String memoryValue ( long inBytes ) + { + return byteCountValue ( inBytes ); + } + + public static final long kSecond = 1000; + public static final long kMinute = 60 * kSecond; + public static final long kHour = 60 * kMinute; + public static final long kDay = 24 * kHour; + public static final long kWeek = 7 * kDay; + public static final long kMonth = 30 * kDay; + public static final long kYear = 365 * kDay; + + public static String numberValue ( long units ) + { + final StringBuffer sb = new StringBuffer (); + + final String raw = "" + units; + final int count = raw.length (); + final int firstPart = count % 3; + int printed = 3 - firstPart; + for ( int i=0; i 0 ) + { + sb.append ( ',' ); + } + printed = 0; + } + sb.append ( raw.charAt ( i ) ); + printed++; + } + + return sb.toString (); + } + + public static String ratioValue ( double d ) + { + // FIXME: use formatter + double rounded2 = Math.round ( d * 100 ) / 100.0; + return "" + rounded2; + } + + public static String pctValue ( double d ) + { + // FIXME: use formatter + final long pct = Math.round ( d * 100 ); + return "" + pct + "%"; + } + + public static String dateValue ( Date d ) + { + return sdf.format ( d ); + } + private static final SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy.MM.dd HH:mm:ss z" ); + + public static String elapsedTimeSince ( Date d ) + { + // return elapsed time with precision that's scaled back as the time grows distant + long unit = 1; + final long elapsedMs = System.currentTimeMillis () - d.getTime (); + + // over 5 seconds, report in seconds + if ( elapsedMs > 1000 * 5 ) + { + unit = 1000; + } + + // over 5 minutes, report in minutes + if ( elapsedMs > 1000*60*5 ) + { + unit = 1000 * 60; + } + + // over 5 hours, report in hours + if ( elapsedMs > 1000*60*60*5 ) + { + unit = 1000 * 60 * 60; + } + + // over 5 days, report in days + if ( elapsedMs > 1000*60*60*24*5 ) + { + unit = 1000 * 60 * 60 * 24; + } + + // over 5 weeks, report in weeks + if ( elapsedMs > 1000*60*60*24*7*5 ) + { + unit = 1000 * 60 * 60 * 24 * 7; + } + + // over 5 months, report in months + if ( elapsedMs > 1000*60*60*24*30*5 ) + { + unit = 1000 * 60 * 60 * 24 * 30; + } + + // over 2 years, report in years + if ( elapsedMs > 1000*60*60*24*365*2 ) + { + unit = 1000 * 60 * 60 * 24 * 365; + } + + return elapsedTimeSince ( d, unit ); + } + + public static String elapsedTimeSince ( Date d, long smallestUnitInMillis ) + { + if ( d == null ) + { + return ""; + } + + final Date now = new Date (); + final long elapsedMs = now.getTime () - d.getTime (); + if ( elapsedMs < 0 ) + { + return timeValue ( elapsedMs * -1, TimeUnit.MILLISECONDS, smallestUnitInMillis ) + " in the future"; + } + else + { + return timeValue ( elapsedMs, TimeUnit.MILLISECONDS, smallestUnitInMillis ) + " ago"; + } + } + + public static String timeValue ( long units, TimeUnit tu, long smallestUnit ) + { + final long timeInMs = TimeUnit.MILLISECONDS.convert ( units, tu ); + + String result = "" + timeInMs + " ms"; + if ( timeInMs > kYear ) + { + final long years = timeInMs / kYear; + final long remaining = timeInMs - ( years * kYear ); + result = "" + years + " yrs"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kMonth ) + { + final long months = timeInMs / kMonth; + final long remaining = timeInMs - ( months * kMonth ); + result = "" + months + " months"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kWeek ) + { + final long weeks = timeInMs / kWeek; + final long remaining = timeInMs - ( weeks * kWeek ); + result = "" + weeks + " wks"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kDay ) + { + final long days = timeInMs / kDay; + final long remaining = timeInMs - ( days * kDay ); + result = "" + days + " days"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kHour ) + { + final long hrs = timeInMs / kHour; + final long remaining = timeInMs - ( hrs * kHour ); + result = "" + hrs + ( hrs == 1 ? " hr" : " hrs" ); + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kMinute ) + { + final long mins = timeInMs / kMinute; + final long remaining = timeInMs - ( mins * kMinute ); + result = "" + mins + " m"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kSecond ) + { + final long seconds = timeInMs / kSecond; + final long remaining = timeInMs - ( seconds * kSecond ); + result = "" + seconds + " s"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else + { + result = "" + timeInMs + " ms"; + } + return result; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaClock.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaClock.java new file mode 100644 index 0000000..1609e8d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaClock.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.util; + +public abstract class NsaClock +{ + /** + * Get the current time from the system clock. + * @return the current time in milliseconds. (Normally equivalent to System.currentTimeMillis()) + */ + public static long now () + { + return getSystemClock().getCurrentMs (); + } + + public synchronized static void setSystemClock ( NsaClock clock ) + { + if ( sfClock != null ) throw new IllegalStateException ( "The clock was already set." ); + sfClock = clock; + } + + public static NsaClock getSystemClock () + { + if ( sfClock == null ) + { + synchronized ( NsaClock.class ) + { + if ( sfClock == null ) // post synch lock, check again. thread may have been behind. + { + sfClock = new NsaJvmClock (); + } + } + } + return sfClock; + } + + public abstract long getCurrentMs (); + + private static NsaClock sfClock = null; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaJvmClock.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaJvmClock.java new file mode 100644 index 0000000..f40c10c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaJvmClock.java @@ -0,0 +1,12 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.util; + +public class NsaJvmClock extends NsaClock +{ + @Override + public long getCurrentMs () { return jvmNow (); } + + public static long jvmNow () { return System.currentTimeMillis (); } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaTestClock.java b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaTestClock.java new file mode 100644 index 0000000..0d5f2eb --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/apiServer/util/NsaTestClock.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer.util; + +public class NsaTestClock extends NsaClock +{ + /** + * Create a test clock starting time 1 and set as the system clock. + */ + public NsaTestClock () + { + this ( 1 ); + } + + /** + * Create a test clock starting at the given time and set as the system clock. + * @param nowMs + */ + public NsaTestClock ( long nowMs ) + { + this ( nowMs, true ); + } + + /** + * Create a test clock starting time 1 and possibly set as the system clock. + * @param nowMs + * @param setAsSystemClock + */ + public NsaTestClock ( long nowMs, boolean setAsSystemClock ) + { + fNowMs = nowMs; + if ( setAsSystemClock ) + { + NsaClock.setSystemClock ( this ); + } + } + + @Override + public synchronized long getCurrentMs () + { + return fNowMs; + } + + public void tick () + { + addMs ( 1 ); + } + + public synchronized void setTo ( long ms ) + { + fNowMs = ms; + } + + public synchronized void addMs ( long ms ) + { + fNowMs += ms; + } + + private long fNowMs; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/cmdLine/NsaCommandLineUtil.java b/saserverlibrary/src/main/java/com/att/nsa/cmdLine/NsaCommandLineUtil.java new file mode 100644 index 0000000..f33ee65 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/cmdLine/NsaCommandLineUtil.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.cmdLine; + +import java.util.HashMap; +import java.util.Map; + +public class NsaCommandLineUtil +{ + /** + * Read a simplified command line format into map. + * @param args + * @return + */ + public static Map processCmdLine ( String[] args ) + { + return processCmdLine ( args, false ); + } + + /** + * Read a simplified command line format into map. + * @param args + * @param stripDashes if true, the returned map for "-foo=bar" would have "foo"="bar" + * @return + */ + public static Map processCmdLine ( String[] args, boolean stripDashes ) + { + final HashMap map = new HashMap (); + + String lastKey = null; + for ( String arg : args ) + { + if ( arg.startsWith ( "-" ) ) + { + if ( lastKey != null ) + { + map.put ( stripDashes ? lastKey.substring(1) : lastKey, "" ); + } + lastKey = arg; + } + else + { + if ( lastKey != null ) + { + map.put ( stripDashes ? lastKey.substring(1) : lastKey, arg ); + } + lastKey = null; + } + } + if ( lastKey != null ) + { + map.put ( stripDashes ? lastKey.substring(1) : lastKey, "" ); + } + return map; + } + + /** + * get a required setting. if not provided, throw IllegalArgumentException with the 'what' description, + * preceded with "You must provide " + * @param argMap + * @param key + * @param what + * @return + */ + public static String getReqdSetting ( Map argMap, String key, String what ) + { + final String val = argMap.get ( key ); + if ( val == null ) + { + throw new IllegalArgumentException ( "You must provide " + what ); + } + return val; + } + + /** + * Get a setting, or, if not provided, return the default value + * @param argMap + * @param key + * @param defaultValue + * @return + */ + public static String getSetting ( Map argMap, String key, String defaultValue ) + { + final String val = argMap.get ( key ); + if ( val == null ) + { + return defaultValue; + } + return val; + } + + /** + * Get a setting, or, if not provided, return the default value + * @param argMap + * @param key + * @param defaultValue + * @return + */ + public static int getSetting ( Map argMap, String key, int defaultValue ) + { + final String s = getSetting ( argMap, key, "" + defaultValue ); + if ( s != null ) + { + try + { + return Integer.parseInt ( s ); + } + catch ( NumberFormatException x ) + { + return defaultValue; + } + } + return defaultValue; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/README.txt b/saserverlibrary/src/main/java/com/att/nsa/drumlin/README.txt new file mode 100644 index 0000000..b616b32 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/README.txt @@ -0,0 +1,4 @@ +The code in the Drumlin package comes from my Apache licensed Drumlin +project, available in full at https://github.com/drumlin. + +-Peter Cardona / peter@rathravane.com / pc569h@att.com diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormFieldInfo.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormFieldInfo.java new file mode 100644 index 0000000..752fc18 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormFieldInfo.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import java.util.LinkedList; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * Information about a form field. + * + * @author peter@rathravane.com + * + */ +public class DrumlinFormFieldInfo +{ + /** + * Construct field info starting with a field name. + * @param fn + */ + public DrumlinFormFieldInfo ( String fn ) + { + fFieldName = fn; + fSteps = new LinkedList (); + } + + /** + * validate form input for this field, given a request context, a form wrapper, + * and an exception to populate (which, if it has a size() > 0, will be thrown by + * the validator). + * + * @param fieldName + * @param context + * @param form + * @param err + */ + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinInvalidFormException err ) + { + for ( DrumlinFormValidationStep step : fSteps ) + { + step.validate ( context, form, this, err ); + } + } + + /** + * Setup validation with a validation step. + * @param step + * @return this + */ + public DrumlinFormFieldInfo validateWith ( DrumlinFormValidationStep step ) + { + fSteps.add ( step ); + return this; + } + + /** + * Note that this field is required. The error message is used when the field + * is missing to populate the validation exception. + * + * @param errMsg + * @return this + */ + public DrumlinFormFieldInfo required ( final String errMsg ) + { + return validateWith ( new DrumlinFormValidationStep () + { + @Override + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ) + { + if ( !form.hasParameter ( fFieldName ) ) + { + err.addProblem ( fFieldName, errMsg ); + } + else + { + final String val = form.getValue ( fFieldName ); + if ( val != null && val.length () == 0 ) + { + err.addProblem ( fFieldName, errMsg ); + } + } + } + } ); + } + + /** + * Note that the field value must be one of the given values. The error message is used + * to populate the validation exception. This is a case-sensitive match. + * @param values + * @param errMsg + * @return this + */ + public DrumlinFormFieldInfo oneOf ( final String[] values, final String errMsg ) + { + return oneOf ( values, true, errMsg ); + } + + /** + * Note that the field value must be one of the given values. The error message is used + * to populate the validation exception. The comparison are case sensitive based on the + * caseSensitive argument. + * @param values + * @param caseSensitive + * @param errMsg + * @return this + */ + public DrumlinFormFieldInfo oneOf ( final String[] values, final boolean caseSensitive, final String errMsg ) + { + return validateWith ( new DrumlinFormValidationStep () + { + @Override + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ) + { + if ( !form.hasParameter ( fFieldName ) ) + { + err.addProblem ( fFieldName, errMsg ); + } + else + { + final String val = form.getValue ( fFieldName ); + + boolean found = false; + for ( String v : values ) + { + if (( caseSensitive && v.equals ( val ) ) || + ( !caseSensitive && v.equalsIgnoreCase ( val ) ) ) + { + found = true; + break; + } + } + + if ( !found ) + { + err.addProblem ( fFieldName, errMsg ); + } + } + } + } ); + } + + /** + * Note that the field value must be one of the given values. The objects are + * converted to strings (via toString) before the comparison, and the comparison + * is case sensitive. + * + * @param values + * @param errMsg + * @return this + */ + public DrumlinFormFieldInfo oneOf ( final Object[] values, final String errMsg ) + { + int current = 0; + final String[] stringVals = new String [ values.length ]; + for ( Object o : values ) + { + stringVals [ current++ ] = o.toString (); + } + return oneOf ( stringVals, errMsg ); + } + + /** + * Note that this field must match the given regular expression. The error message + * is used to populate the validation exception when the match fails. + * @param regex + * @param errMsg + * @return this + */ + public DrumlinFormFieldInfo matches ( final String regex, final String errMsg ) + { + return validateWith ( new DrumlinFormValidationStep () + { + @Override + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ) + { + String value = form.getValue ( fFieldName ); + if ( value == null ) + { + value = ""; + } + if ( !value.matches ( regex ) ) + { + err.addProblem ( fFieldName, errMsg ); + } + } + } ); + } + + /** + * Provide this field with a default value that's used when the form does not + * contain a value. Note that validation steps are run in the order they're + * created, so using required() before defaultValue() is probably not what + * you'd want. + * + * @param defVal + * @return this + */ + public DrumlinFormFieldInfo defaultValue ( final String defVal ) + { + return validateWith ( new DrumlinFormValidationStep () + { + @Override + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ) + { + if ( !form.hasParameter ( fFieldName ) || + ( form.isFormField ( fFieldName ) && form.getValue ( fFieldName ).length () == 0 ) ) + { + form.changeValue ( fFieldName, defVal ); + } + } + } ); + } + + /** + * Provide a default value for this field. Note that the object is converted to a string. + * (This is purely a convenience method.) + * @param o + * @return this + */ + public DrumlinFormFieldInfo defaultValue ( final Object o ) + { + return defaultValue ( o.toString () ); + } + + /** + * Note the field value must be one of a set of known boolean equivalent strings: + * true/false, yes/no, on/off, 1/0, and checked. + * @param errMsg + * @return + */ + public DrumlinFormFieldInfo isBoolean ( final String errMsg ) + { + return oneOf ( new String[] { "true", "false", "yes", "no", "on", "off", "1", "0", "checked" }, false, errMsg ); + } + + public final String fFieldName; + private LinkedList fSteps; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormPostWrapper.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormPostWrapper.java new file mode 100644 index 0000000..6fdcaa6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormPostWrapper.java @@ -0,0 +1,574 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.app.htmlForms.mime.DrumlinMimePart; +import com.att.nsa.drumlin.app.htmlForms.mime.DrumlinMimePartFactory; +import com.att.nsa.drumlin.app.htmlForms.mime.DrumlinMimePartsReader; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.service.standards.HttpMethods; +import com.att.nsa.drumlin.till.collections.rrMultiMap; +import com.att.nsa.drumlin.till.data.rrConvertor; + +/** + * A form post wrapper provides form related methods over a DrumlinRequest. + * + * @author peter@rathravane.com + * + */ +public class DrumlinFormPostWrapper +{ + /** + * Construct a form post wrapper from a request. + * @param req + */ + public DrumlinFormPostWrapper ( DrumlinRequest req ) + { + this ( req, null ); + } + + /** + * Construct a form post wrapper from a request, and use the given MIME reader + * part factory. The MIME reader is only invoked if the request's content type + * is multipart/form-data. + * + * @param req + * @param mimePartFactory If null, use the built-in part factory. + */ + public DrumlinFormPostWrapper ( DrumlinRequest req, DrumlinMimePartFactory mimePartFactory ) + { + fRequest = req; + final String ct = req.getContentType (); + + fIsMultipartFormData = ct != null && ct.startsWith ( "multipart/form-data" ); + fPartFactory = mimePartFactory == null ? new simpleStorage () : mimePartFactory; + fParsedValues = new HashMap (); + fParseComplete = false; + } + + /** + * this must be called to cleanup mime part resources (e.g. tmp files) + */ + public void close () + { + for ( DrumlinMimePart vi : fParsedValues.values () ) + { + vi.discard (); + } + } + + @Override + public String toString () + { + final StringBuffer sb = new StringBuffer (); + + sb.append ( fRequest.getMethod ().toUpperCase () ).append ( " {" ); + if ( fIsMultipartFormData ) + { + if ( fParseComplete ) + { + for ( Entry e : fParsedValues.entrySet () ) + { + sb.append ( e.getKey () + ":" ); + final DrumlinMimePart mp = e.getValue (); + if ( mp.getAsString () != null ) + { + sb.append ( "'" + mp.getAsString () + "' " ); + } + else + { + sb.append ( "(data) " ); + } + } + } + else + { + sb.append ( "not parsed yet" ); + } + } + else + { + for ( Entry e : fRequest.getParameterMap().entrySet () ) + { + sb.append ( e.getKey ().toString () + ":'" + e.getValue ().toString () + "' " ); + } + } + sb.append ( " }" ); + + return sb.toString (); + } + + /** + * Is the underlying request a POST? (Not a PUT, not anything else. Just POST.) + * @return true if the underlying request is a POST. + */ + public boolean isPost () + { + return fRequest.getMethod ().toLowerCase ().equals ( HttpMethods.POST ); + } + + /** + * Does the form have a given parameter (aka field) + * @param name + * @return true if the named parameter/field exists in the form post + */ + public boolean hasParameter ( String name ) + { + parseIfNeeded (); + + return fIsMultipartFormData ? + fParsedValues.containsKey ( name ) : + fRequest.getParameterMap ().containsKey ( name ); + } + + /** + * Get the form post parameters in a map from name to string value. + * @return a map of post parameters + */ + public Map getValues () + { + final HashMap map = new HashMap (); + + parseIfNeeded (); + + if ( fIsMultipartFormData ) + { + for ( Map.Entry e : fParsedValues.entrySet () ) + { + final String val = e.getValue ().getAsString (); + if ( val != null ) + { + map.put ( e.getKey(), val ); + } + } + } + else + { + for ( Map.Entry e : fRequest.getParameterMap ().entrySet() ) + { + final String key = e.getKey ().toString (); + final String[] vals = (String[]) e.getValue (); + String valToUse = ""; + if ( vals.length > 0 ) + { + valToUse = vals[0]; + } + map.put ( key, valToUse ); + } + } + return map; + } + + /** + * Does the form contain the given field? This goes beyond hasParameter() to check + * on a multipart MIME post whether the value provided is a string. + * + * @param name + * @return true if the named field exists + */ + public boolean isFormField ( String name ) + { + boolean result = false; + if ( hasParameter ( name ) ) + { + if ( fIsMultipartFormData ) + { + final DrumlinMimePart val = fParsedValues.get ( name ); + result = ( val != null && val.getAsString () != null ); + } + else + { + result = true; + } + } + return result; + } + + /** + * Get the value of a field as a string. This returns null for MIME parts like + * file uploads -- the value has to be available as a string rather than a stream. + * + * @param name + * @return a string for the named field + */ + public String getValue ( String name ) + { + parseIfNeeded (); + + String result = null; + if ( fIsMultipartFormData ) + { + final DrumlinMimePart val = fParsedValues.get ( name ); + + result = null; + if ( val != null && val.getAsString () != null ) + { + result = val.getAsString ().trim (); + } + } + else + { + result = fRequest.getParameter ( name ); + if ( result != null ) + { + result = result.trim (); + } + } + return result; + } + + /** + * A convenience version of getValue(String). Useful for passing enums. The argument + * is converted to a string. + * @param o + * @return the string value for the given field + */ + public String getValue ( Object o ) + { + return getValue ( o.toString () ); + } + + /** + * Get the named value, or return defVal if it does not exist on the form. + * @param key + * @param defVal + * @return the value from the form, or the default value + */ + public String getValue ( String key, String defVal ) + { + String result = getValue ( key ); + if ( result == null ) + { + result = defVal; + } + return result; + } + + /** + * A convenience version for use with Enums. The field name argument is converted to a string. + * @param fieldName + * @param defVal + * @return + */ + public String getValue ( Object fieldName, String defVal ) + { + return getValue ( fieldName.toString (), defVal ); + } + + /** + * Get the named value as a boolean, or return valIfMissing if no such field exists. + * @param name + * @param valIfMissing + * @return true/false + */ + public boolean getValueBoolean ( String name, boolean valIfMissing ) + { + boolean result = valIfMissing; + final String val = getValue ( name ); + if ( val != null ) + { + result = rrConvertor.convertToBooleanBroad ( val ); + } + return result; + } + + /** + * A convenience version for use with Enums. The field name argument is converted to a string. + * @param fieldName + * @param valIfMissing + * @return true/false + */ + public boolean getValueBoolean ( Object fieldName, boolean valIfMissing ) + { + return getValueBoolean ( fieldName.toString() , valIfMissing ); + } + + /** + * Change the value for a given field. + * @param fieldName + * @param newVal + */ + public void changeValue ( String fieldName, String newVal ) + { + parseIfNeeded (); + + if ( fIsMultipartFormData ) + { + if ( fParsedValues.containsKey ( fieldName ) ) + { + fParsedValues.get ( fieldName ).discard (); + } + + final inMemoryFormDataPart part = new inMemoryFormDataPart ( "", "form-data; name=\"" + fieldName + "\"" ); + final byte[] array = newVal.getBytes (); + part.write ( array, 0, array.length ); + part.close (); + fParsedValues.put ( fieldName, part ); + } + else + { + fRequest.changeParameter ( fieldName, newVal ); + } + } + + /** + * Get the MIME part for a given field name. + * @param name + * @return a MIME part + */ + public DrumlinMimePart getStream ( String name ) + { + parseIfNeeded (); + + DrumlinMimePart result = null; + if ( fIsMultipartFormData ) + { + final DrumlinMimePart val = fParsedValues.get ( name ); + if ( val != null && val.getAsString () == null ) + { + return val; + } + } + return result; + } + + private final DrumlinRequest fRequest; + private final boolean fIsMultipartFormData; + private boolean fParseComplete; + private final HashMap fParsedValues; + private final DrumlinMimePartFactory fPartFactory; + + private void parseIfNeeded () + { + if ( fIsMultipartFormData && !fParseComplete ) + { + try + { + final String ct = fRequest.getContentType (); + int boundaryStartIndex = ct.indexOf ( kBoundaryTag ); + if ( boundaryStartIndex != -1 ) + { + boundaryStartIndex = boundaryStartIndex + kBoundaryTag.length (); + final int semi = ct.indexOf ( ";", boundaryStartIndex ); + int boundaryEndIndex = semi == -1 ? ct.length () : semi; + + final String boundary = ct.substring ( boundaryStartIndex, boundaryEndIndex ).trim (); + final DrumlinMimePartsReader mmr = new DrumlinMimePartsReader ( boundary, fPartFactory ); + final InputStream is = fRequest.getBodyStream (); + mmr.read ( is ); + is.close (); + + for ( DrumlinMimePart mp : mmr.getParts () ) + { + fParsedValues.put ( mp.getName(), mp ); + } + } + } + catch ( IOException e ) + { + log.warn ( "There was a problem reading a multipart/form-data POST: " + e.getMessage () ); + } + fParseComplete = true; + } + } + + private static final String kBoundaryTag = "boundary="; + + static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinFormPostWrapper.class ); + + public static abstract class basePart implements DrumlinMimePart + { + public basePart ( String contentType, String contentDisp ) + { + fType = contentType; + fDisp = contentDisp; + + fDispMap = new HashMap (); + parseDisposition ( contentDisp ); + + final int nameSpot = fDisp.indexOf ( "name=\"" ); + String namePart = fDisp.substring ( nameSpot + "name=\"".length () ); + final int closeQuote = namePart.indexOf ( "\"" ); + namePart = namePart.substring ( 0, closeQuote ); + fName = namePart; + } + + @Override + public String getContentType () + { + return fType; + } + + @Override + public String getContentDisposition () + { + return fDisp; + } + + @Override + public String getContentDispositionValue ( String key ) + { + return fDispMap.get ( key ); + } + + @Override + public String getName () + { + return fName; + } + + @Override + public void discard () + { + } + + private final String fType; + private final String fDisp; + private final String fName; + private final HashMap fDispMap; + + // form-data; name="file"; filename="IMG_21022013_122919.png" + private void parseDisposition ( String contentDisp ) + { + final String[] parts = contentDisp.split ( ";"); + for ( String part : parts ) + { + String key = part.trim (); + String val = ""; + final int eq = key.indexOf ( '=' ); + if ( eq > -1 ) + { + val = key.substring ( eq+1 ); + key = key.substring ( 0, eq ); + + // if val is in quotes, remove them + if ( val.startsWith ( "\"" ) && val.endsWith ( "\"" ) ) + { + val = val.substring ( 1, val.length () - 1 ); + } + } + fDispMap.put ( key, val ); + } + } + } + + public static class inMemoryFormDataPart extends basePart + { + public inMemoryFormDataPart ( String ct, String cd ) + { + super ( ct, cd ); + fValue = ""; + } + + @Override + public void write ( byte[] line, int offset, int length ) + { + fValue = new String ( line, offset, length ); + } + + @Override + public void close () + { + } + + @Override + public InputStream openStream () throws IOException + { + throw new IOException ( "Opening stream on in-memory form data." ); + } + + @Override + public String getAsString () + { + return fValue; + } + + private String fValue; + } + + private static class tmpFilePart extends basePart + { + public tmpFilePart ( String ct, String cd ) throws IOException + { + super ( ct, cd ); + + fFile = File.createTempFile ( "drumlin.", ".part" ); + fStream = new FileOutputStream ( fFile ); + } + + @Override + public void write ( byte[] line, int offset, int length ) throws IOException + { + if ( fStream != null ) + { + fStream.write ( line, offset, length ); + } + } + + @Override + public void close () throws IOException + { + if ( fStream != null ) + { + fStream.close (); + fStream = null; + } + } + + @Override + public InputStream openStream () throws IOException + { + if ( fStream != null ) + { + log.warn ( "Opening input stream on tmp file before it's fully written." ); + } + return new FileInputStream ( fFile ); + } + + @Override + public String getAsString () + { + return null; + } + + @Override + public void discard () + { + fFile.delete (); + fFile = null; + fStream = null; + } + + private File fFile; + private FileOutputStream fStream; + } + + static class simpleStorage implements DrumlinMimePartFactory + { + @Override + public DrumlinMimePart createPart ( rrMultiMap partHeaders ) throws IOException + { + final String contentDisp = partHeaders.getFirst ( "content-disposition" ); + if ( contentDisp != null && contentDisp.contains ( "filename=\"" ) ) + { + return new tmpFilePart ( partHeaders.getFirst ( "content-type" ), contentDisp ); + } + else + { + return new inMemoryFormDataPart ( partHeaders.getFirst ( "content-type" ), contentDisp ); + } + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidationStep.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidationStep.java new file mode 100644 index 0000000..44f2e94 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidationStep.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A form validation step. + * @author peter@rathravane.com + * + */ +public interface DrumlinFormValidationStep +{ + /** + *

Given a context, form, and field information, decide if the value is valid. If it is not, + * add an error listing for the field to the supplied validation error. (Once all + * validation steps are complete, if the error object contains any errors, it's thrown as + * an exception, indicating a problem with the form submission.)

+ * + *

For form-level validation, the field argument is null.

+ * + * @param context + * @param form + * @param field Field info, or null for form-level validation. + * @param err + */ + void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidator.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidator.java new file mode 100644 index 0000000..8af4f7e --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinFormValidator.java @@ -0,0 +1,135 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A form validation tool. + * @author peter@rathravane.com + * + */ +public class DrumlinFormValidator +{ + /** + * Construct an empty form validator. + */ + public DrumlinFormValidator () + { + this ( null ); + } + + /** + * Construct a form validator using another form validator as a starting point. + * This is useful for "wizard" UIs that carry form data along through each step. + * The validator for step 2 is based on the validator for step 1, step 3 is based + * on step 2, etc. + * + * @param wrapped + */ + public DrumlinFormValidator ( DrumlinFormValidator wrapped ) + { + fMap = new HashMap (); + fValidators = new LinkedList (); + + if ( wrapped != null ) + { + addValidation ( new wrapper ( wrapped ) ); + } + } + + /** + *

Validate the given form (referenced by the form wrapper) using the validators + * registered on this object. Validation is done at the field level first, then + * at the form level.

+ * + *

Validation problems are collected into an exception. If the exception instance + * contains any errors after all validation steps complete, it's thrown.

+ * + * @param context + * @param w + * @throws DrumlinInvalidFormException if a validation step fails + */ + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper w ) throws DrumlinInvalidFormException + { + final DrumlinInvalidFormException ve = new DrumlinInvalidFormException (); + for ( DrumlinFormFieldInfo fi : fMap.values () ) + { + fi.validate ( context, w, ve ); + } + for ( DrumlinFormValidationStep step : fValidators ) + { + step.validate ( context, w, null, ve ); + } + if ( ve.size () > 0 ) + { + throw ve; + } + } + + /** + * Get the fields known to this validator, along with their field info objects. + * @return a map from field name to field info + */ + public Map getFields () + { + return fMap; + } + + /** + * Get or create a field info object for a named field. Then add validation requirements + * to the field. + * @param name + * @return a field info object + */ + public DrumlinFormFieldInfo field ( String name ) + { + DrumlinFormFieldInfo fi = fMap.get ( name ); + if ( fi == null ) + { + fi = new DrumlinFormFieldInfo ( name ); + fMap.put ( name, fi ); + } + return fi; + } + + /** + * Add a form-level validation step to this validator. + * @param step + */ + public void addValidation ( DrumlinFormValidationStep step ) + { + fValidators.add ( step ); + } + + private final HashMap fMap; + private final LinkedList fValidators; + + private class wrapper implements DrumlinFormValidationStep + { + public wrapper ( DrumlinFormValidator step ) + { + fWrapped = step; + } + + @Override + public void validate ( DrumlinRequestContext context, DrumlinFormPostWrapper form, DrumlinFormFieldInfo field, DrumlinInvalidFormException err ) + { + try + { + fWrapped.validate ( context, form ); + } + catch ( DrumlinInvalidFormException e ) + { + err.addProblemsFrom ( e ); + } + } + + private DrumlinFormValidator fWrapped; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinInvalidFormException.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinInvalidFormException.java new file mode 100644 index 0000000..6e28a7a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinInvalidFormException.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.att.nsa.drumlin.till.collections.rrMultiMap; + +/** + * An exception signalling that a form is invalid. It carries specific problems + * (and warnings) by field name, and separately for the overall form. It's intended + * to be caught and used to report problems back to the user. + * + * @author peter@rathravane.com + */ +public class DrumlinInvalidFormException extends Exception +{ + public DrumlinInvalidFormException () + { + super ( "Form validation errors" ); + fFieldProblems = new rrMultiMap (); + fFieldWarnings = new rrMultiMap (); + fFormProblems = new LinkedList (); + } + + /** + * Add a general problem. + * @param problem + */ + public void addProblem ( String problem ) + { + fFormProblems.add ( problem ); + } + + /** + * Add a problem with a specific field. + * @param field + * @param problem + */ + public void addProblem ( String field, String problem ) + { + fFieldProblems.put ( field, problem ); + } + + /** + * Add a warning for a specific field. + * @param field + * @param problem + */ + public void addWarning ( String field, String problem ) + { + fFieldWarnings.put ( field, problem ); + } + + /** + * Copy problems from another invalid form exception. + * @param that + */ + public void addProblemsFrom ( DrumlinInvalidFormException that ) + { + fFormProblems.addAll ( that.getFormProblems () ); + fFieldProblems.putAll ( that.getFieldProblems () ); + } + + /** + * Get the count of problems. + * @return the count of problems. + */ + public int size () + { + return fFieldProblems.size () + fFormProblems.size (); + } + + /** + * Get all field problems. + * @return a map from field name to a list of problem strings + */ + public Map> getFieldProblems () + { + return fFieldProblems.getValues (); + } + + /** + * Get problems on a particular field. + * @param field + * @return a list of 0 or more problems. + */ + public List getProblemsOn ( String field ) + { + final LinkedList list = new LinkedList (); + final List vals = fFieldProblems.get ( field ); + if ( vals != null ) + { + list.addAll ( vals ); + } + return list; + } + + /** + * Get all field warnings. + * @return a map from field name to a list of warning strings + */ + public Map> getFieldWarnings () + { + return fFieldWarnings.getValues (); + } + + /** + * Get the form-level problems. + * @return a list of form problems + */ + public List getFormProblems () + { + return fFormProblems; + } + + private final LinkedList fFormProblems; + private rrMultiMap fFieldProblems; + private rrMultiMap fFieldWarnings; + private static final long serialVersionUID = 1L; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinRequestReader.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinRequestReader.java new file mode 100644 index 0000000..e09b8fb --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/DrumlinRequestReader.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; + +/** + * A request reader. Is this in use?? + * @author peter@rathravane.com + * + */ +@Deprecated +public class DrumlinRequestReader +{ + public static class invalidRequest extends Exception + { + public invalidRequest ( String msg ) + { + super ( msg ); + } + private static final long serialVersionUID = 1L; + } + + public static class badInputRequest extends invalidRequest + { + public badInputRequest ( String key, String msg ) + { + super ( msg ); + fKey = key; + fMsg = msg; + } + public final String fKey; + public final String fMsg; + private static final long serialVersionUID = 1L; + } + + public DrumlinRequestReader () + { + fFieldMap = new HashMap (); + } + + public void registerArgument ( String key, boolean reqd, boolean notEmpty, String defVal, String[] limitTo ) + { + final fieldInfo fi = new fieldInfo (); + fi.fDefVal = defVal; + fi.fReqd = reqd; + fi.fNotEmpty = notEmpty; + fi.fValueLimitedTo = limitTo; + + fFieldMap.put ( key, fi ); + } + + public Map read ( HttpServletRequest req ) throws invalidRequest + { + final HashMap valueMap = new HashMap (); + + // start with required defaults + for ( final Entry e : fFieldMap.entrySet () ) + { + final String key = e.getKey (); + final fieldInfo fi = e.getValue (); + if ( fi.fReqd && fi.fDefVal != null ) + { + valueMap.put ( key, fi.fDefVal ); + } + } + + // read the input + final Map params = req.getParameterMap (); + for ( Entry e : params.entrySet () ) + { + final String p = e.getKey().toString (); + final String[] vArray = (String[]) e.getValue(); + final String v = vArray.length > 0 ? vArray[0] : null; + + final fieldInfo fi = fFieldMap.get ( p ); + if ( fi != null ) + { + if ( fi.fValueLimitedTo != null ) + { + boolean found = false; + for ( int i=0; !found && i e : fFieldMap.entrySet () ) + { + final String key = e.getKey (); + final fieldInfo fi = e.getValue (); + if ( fi.fReqd ) + { + final String val = valueMap.get ( key ); + if ( val == null || ( fi.fNotEmpty && val.length() == 0 ) ) + { + throw new badInputRequest ( key, "Please provide a value!" ); + } + } + else if ( fi.fNotEmpty ) + { + final String val = valueMap.get ( key ); + if ( val != null && val.length() == 0 ) + { + throw new badInputRequest ( key, "Please provide a value!" ); + } + } + } + + return valueMap; + } + + private class fieldInfo + { + public fieldInfo () + { + fDefVal = null; + fReqd = false; + fValueLimitedTo = null; + } + + public String fDefVal; + public boolean fReqd; + public boolean fNotEmpty; + public String[] fValueLimitedTo; + } + private HashMap fFieldMap; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePart.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePart.java new file mode 100644 index 0000000..0ddd9e2 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePart.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms.mime; + +import java.io.IOException; +import java.io.InputStream; + +/** + * A MIME part. These are created by the multipart MIME reader via the supplied + * part factory. + * + * @author peter@rathravane.com + * + */ +public interface DrumlinMimePart +{ + /** + * Get the content type for this part. + * @return a type string + */ + String getContentType (); + + /** + * Get the content disposition value for this part. + * @return a content disposition string + */ + String getContentDisposition (); + + /** + * Get the name for this part. + * @return a name string + */ + String getName (); + + /** + * Get the value associated with a given content disposition key. If the value doesn't + * exist, null is returned. If the value is not provided, an empty string is returned. + * @param key + * @return a string or null if undefined + */ + String getContentDispositionValue ( String key ); + + /** + * open a stream to read this part's data. + * @return an input stream + * @throws IOException + */ + InputStream openStream () throws IOException; + + /** + * Get this part's data as a string. + * @return the part data as a string + */ + String getAsString (); + + /** + * Discard this part. + */ + void discard (); + + /** + * Used by the MIME reader to write bytes to the part. + * @param line + * @param offset + * @param length + * @throws IOException + */ + void write ( byte[] line, int offset, int length ) throws IOException; + + /** + * Used by the MIME reader to close the part during its creation. + * @throws IOException + */ + void close () throws IOException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartFactory.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartFactory.java new file mode 100644 index 0000000..4f68f7b --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartFactory.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms.mime; + +import java.io.IOException; + +import com.att.nsa.drumlin.till.collections.rrMultiMap; + +/** + * A MIME part factory. The factory is provided to the multipart MIME reader to + * allow an application to create parts. For example, a web app receiving a file + * input may want to store that file on AWS S3 rather than in a local tmp file. + * + * @author peter@rathravane.com + * + */ +public interface DrumlinMimePartFactory +{ + /** + * Create a MIME part given header values for the part section. + * @param partHeaders + * @return a new MIME part + * @throws IOException + */ + DrumlinMimePart createPart ( rrMultiMap partHeaders ) throws IOException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartsReader.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartsReader.java new file mode 100644 index 0000000..492b668 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/htmlForms/mime/DrumlinMimePartsReader.java @@ -0,0 +1,364 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.htmlForms.mime; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.till.collections.rrMultiMap; +import com.att.nsa.drumlin.till.data.humanReadableHelper; +import com.att.nsa.drumlin.till.data.stringUtils; + +/** + * A multipart MIME reader. + * + * @author peter@rathravane.com + */ +public class DrumlinMimePartsReader +{ + /** + * Construct a multipart MIME reader with a boundary string and a part factory. + * @param boundary + * @param mpf + */ + public DrumlinMimePartsReader ( String boundary, DrumlinMimePartFactory mpf ) + { + fBoundaryLine = "--" + boundary; + fBoundaryEndMarker = fBoundaryLine + "--"; + fPartIndex = -1; + fInPartHeader = false; + fPartHeaders = null; + fPartFactory = mpf; + fCurrentPart = null; + fAllParts = new ArrayList (); + } + + /** + * Read the given input stream. The stream is read to the end, but + * left open for the caller to close. + * + * @param in + * @throws IOException + */ + public void read ( InputStream in ) throws IOException + { + final BufferedInputStream bis = new BufferedInputStream ( in ); + + String line = null; + while ( ( line = readLine ( bis ) ) != null ) + { + if ( line.equals ( fBoundaryLine ) ) + { + onPartBoundary ( ++fPartIndex ); + fInPartHeader = true; + fPartHeaders = new rrMultiMap (); + } + else if ( line.equals ( fBoundaryEndMarker ) ) + { + onPartBoundary ( ++fPartIndex ); + onStreamEnd (); + break; + } + else if ( fPartIndex == -1 ) + { + // header info, discard + } + else if ( fInPartHeader && line.length() == 0 ) + { + // switch from header info to body + fInPartHeader = false; + onPartHeaders ( fPartHeaders ); + + // now read until the next part boundary... + readPartBytes ( bis ); + + // here, we expect CRLF prior to the next part boundary + line = readLine ( bis ); + if ( line == null || line.length() > 0 ) + { + log.warn ( "Unexpected state in MIME reader. After MIME part, found line [" + line + "]." ); + } + } + else if ( fInPartHeader ) + { + // part header line + final int colon = line.indexOf ( ':' ); + if ( colon == -1 ) + { + // weird. ignore. + } + else + { + final String key = line.substring ( 0, colon ).trim ().toLowerCase (); + final String val = line.substring ( colon + 1 ).trim (); + fPartHeaders.put ( key, val ); + } + } + else + { + // hmm + log.warn ( "Unexpected state in MIME reader." ); + } + } + } + + /** + * Get the MIME parts read by this reader. + * @return a list of 0 or more MIME parts + */ + public List getParts () + { + return fAllParts; + } + + /** + * Parse a content disposition string into a multimap. + * @param cd + * @return a multimap wth entries from the disposition string + */ + public static rrMultiMap parseContentDisposition ( String cd ) + { + // e.g. Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg" + final rrMultiMap result = new rrMultiMap (); + final String[] parts = cd.split ( ";" ); + if ( parts.length > 0 ) + { + // first part is special -- it's the disposition (e.g. "attachment") + result.put ( "disposition", parts[0] ); + for ( int i=1; i headers ) throws IOException + { + closeCurrentPart (); + fCurrentPart = fPartFactory.createPart ( headers ); + } + + /** + * Called multiple times during the read of a part's body. + * @param line + * @throws IOException + */ + protected void onPartBytes ( byte[] line, int offset, int length ) throws IOException + { + if ( fCurrentPart != null ) + { + fCurrentPart.write ( line, offset, length ); + } + } + + /** + * Called when the multipart stream is complete + */ + protected void onStreamEnd () + { + } + + private final String fBoundaryLine; + private final String fBoundaryEndMarker; + private int fPartIndex; + private boolean fInPartHeader; + private rrMultiMap fPartHeaders; + private final DrumlinMimePartFactory fPartFactory; + private DrumlinMimePart fCurrentPart; + private final ArrayList fAllParts; + + private static final int kPartBytesBufferSize = 2048; + + private void closeCurrentPart () throws IOException + { + if ( fCurrentPart != null ) + { + fCurrentPart.close (); + fAllParts.add ( fCurrentPart ); + fCurrentPart = null; + } + } + + private static String readLine ( BufferedInputStream bis ) throws IOException + { + // in this mode, we're looking for a line ending + final ByteArrayOutputStream baos = new ByteArrayOutputStream (); + boolean eol = false; + while ( !eol ) + { + int b = bis.read (); + if ( b == -1 ) + { + break; + } + else + { + if ( b == '\r' || b == '\n' ) + { + eol = true; + bis.mark ( 1 ); + + // eat a \r\n just like \r or \n + if ( b == '\r' ) + { + b = bis.read (); + if ( b != '\n' ) + { + bis.reset (); + } + } + } + else + { + baos.write ( b ); + } + } + } + + String result = ""; + if ( baos.size () > 0 ) + { + result = new String ( baos.toByteArray () ); + } + return result; + } + + private void readPartBytes ( BufferedInputStream bis ) throws IOException + { + final byte[] buffer = new byte [ kPartBytesBufferSize ]; + int readSoFar = 0; + long readTotal = 0L; + + final int boundaryTagLen = fBoundaryLine.length () + 2; // 2 for preceding CRLF + final byte[] boundaryLineBytes = ("\r\n" + fBoundaryLine).getBytes(); + + // read until the boundary line is found. we have to inspect each byte + // so just read them one at a time to keep the code simple + while ( true ) + { + bis.mark ( 2 ); + + final int b = bis.read (); + if ( b == '\r' ) + { + // this could start a part boundary. see if we can read it. + + // first deliver the current buffer + onPartBytes ( buffer, 0, readSoFar ); + readSoFar = 0; + + // now see what's here. + bis.reset (); + bis.mark ( boundaryTagLen ); + final int read = bis.read ( buffer, 0, boundaryTagLen ); + if ( read == boundaryTagLen && startsWith ( buffer, boundaryLineBytes ) ) + { + bis.reset (); + return; + } + else + { + // nevermind, continue, but only consume the hyphen so that + // the following bytes are processed properly. + bis.reset (); + buffer[ readSoFar++ ] = (byte)((bis.read()) & 0xff); + readTotal++; + } + } + else if ( b == -1 ) + { + // this is a mulitpart stream read. it's required to end with a part boundary before + // the stream is complete. + throw new IOException ( "Stream ended without part boundary." ); + } + else + { + buffer[ readSoFar++ ] = (byte)(b & 0xff); + readTotal++; + if ( readSoFar == 2048 ) + { + onPartBytes ( buffer, 0, readSoFar ); + readSoFar = 0; + } + } + + if ( readTotal % (1024*1024) == 0 ) + { + log.info ( humanReadableHelper.byteCountValue ( readTotal ) + " read" ); + } + } + } + + /** + * Check if one byte array starts with another. Equivalent to startsWith(source,0,match); + * @param source + * @param match + * @return true if source starts with match + */ + public static boolean startsWith ( byte[] source, byte[] match ) + { + return startsWith ( source, 0, match ); + } + + /** + * Check if one byte array contains another at the given offset. + * @param source + * @param offset + * @param match + * @return true if source contains match at the offset + */ + public static boolean startsWith ( byte[] source, int offset, byte[] match ) + { + if ( match.length > ( source.length - offset ) ) + { + return false; + } + + for ( int i = 0; i < match.length; i++ ) + { + if ( source[offset + i] != match[i] ) + { + return false; + } + } + return true; + } + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinMimePartsReader.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/browser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/browser.java new file mode 100644 index 0000000..f63998d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/browser.java @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.app.userAgents.browsers; + +public interface browser +{ + String getName (); + String getVersion (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/chromeBrowser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/chromeBrowser.java new file mode 100644 index 0000000..193cfbf --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/chromeBrowser.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.browsers; + +public class chromeBrowser extends genericBrowser +{ + public chromeBrowser () + { + super ( "Chrome", "" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/firefoxBrowser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/firefoxBrowser.java new file mode 100644 index 0000000..197b016 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/firefoxBrowser.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.browsers; + +public class firefoxBrowser extends genericBrowser +{ + public firefoxBrowser () + { + super ( "Firefox", "" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/genericBrowser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/genericBrowser.java new file mode 100644 index 0000000..469aa01 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/genericBrowser.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.browsers; + +public class genericBrowser implements browser +{ + public genericBrowser () + { + this ( "generic", "" ); + } + + public genericBrowser ( String name, String version ) + { + fName = name; + fVersion = version; + } + + @Override + public String getName () + { + return fName; + } + + @Override + public String getVersion () + { + return fVersion; + } + + private final String fName; + private final String fVersion; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/safariBrowser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/safariBrowser.java new file mode 100644 index 0000000..6843983 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/browsers/safariBrowser.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.browsers; + +public class safariBrowser extends genericBrowser +{ + public safariBrowser () + { + super ( "Safari", "" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/deviceDetector.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/deviceDetector.java new file mode 100644 index 0000000..ad61737 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/deviceDetector.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents; + +import com.att.nsa.drumlin.app.userAgents.browsers.chromeBrowser; +import com.att.nsa.drumlin.app.userAgents.browsers.firefoxBrowser; +import com.att.nsa.drumlin.app.userAgents.browsers.genericBrowser; +import com.att.nsa.drumlin.app.userAgents.browsers.safariBrowser; +import com.att.nsa.drumlin.app.userAgents.devices.genericDevice; +import com.att.nsa.drumlin.app.userAgents.devices.android.androidDevice; +import com.att.nsa.drumlin.app.userAgents.devices.computers.macintosh; +import com.att.nsa.drumlin.app.userAgents.devices.ios.iPhone; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public class deviceDetector +{ + public static final String kSetting_DebugForceDevice = "drumlin.deviceDetect.force"; + + public static userAgent detect ( DrumlinRequest r, rrNvReadable settings ) + { + return detect ( userAgentInfo.analyze ( r ), settings ); + } + + public static userAgent detect ( userAgentInfo uaa, rrNvReadable settings ) + { + final String forced = settings.getString ( kSetting_DebugForceDevice, null ); + if ( forced != null ) + { + if ( forced.equalsIgnoreCase ( "genericBrowser" ) ) + { + return new genericAgent ( new genericDevice ( false ), new genericBrowser () ); + } + else if ( forced.equalsIgnoreCase ( "genericMobile" ) ) + { + return new genericAgent ( new genericDevice ( true ), new genericBrowser () ); + } + } + + // FIXME: do this in a more robust way, but for now, just get it done for some simple known devices + + if ( uaa.hasFeature ( "Mozilla" ) && uaa.getFeatureVersion ( "Mozilla", 0.0 ) >= 5.0 ) + { + final String comment = uaa.getFeatureComment ( "Mozilla" ); + if ( comment.contains ( "iPhone" ) ) + { + if ( uaa.hasFeature ( "CriOS" ) ) + { + return new genericAgent ( new iPhone (), new chromeBrowser () ); + } + else if ( uaa.hasFeature ( "Safari" ) ) + { + return new genericAgent ( new iPhone (), new safariBrowser () ); + } + } + else if ( comment.contains ( "Android" ) ) + { + return new genericAgent ( new androidDevice (), new genericBrowser () ); + } + else if ( comment.contains ( "Macintosh" ) ) + { + if ( uaa.hasFeature ( "Chrome" ) ) + { + return new genericAgent ( new macintosh(), new chromeBrowser () ); + } + else if ( uaa.hasFeature ( "Safari" ) ) + { + return new genericAgent ( new macintosh(), new safariBrowser () ); + } + else if ( uaa.hasFeature ( "Firefox" ) ) + { + return new genericAgent ( new macintosh(), new firefoxBrowser () ); + } + } + else if ( comment.contains ( "Linux" ) ) + { + if ( uaa.hasFeature ( "Chrome" ) ) + { + return new genericAgent ( new genericDevice(), new chromeBrowser () ); + } + else if ( uaa.hasFeature ( "Firefox" ) ) + { + return new genericAgent ( new genericDevice(), new firefoxBrowser () ); + } + } + } + + return new genericAgent (); + } + + // NOTES: + // + // Chrome browser on iPhone 3: User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X; en-us) AppleWebKit/536.26 (KHTML, like Gecko) CriOS/23.0.1271.100 Mobile/10A403 Safari/8536.25 + // Safari browser on iPhone 3: User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A403 Safari/8536.25 + // + // Firefox on Linux: User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:17.0) Gecko/20100101 Firefox/17.0 + // + // Chrome on Mac OSX: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.101 Safari/537.11 + // + +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/android/androidDevice.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/android/androidDevice.java new file mode 100644 index 0000000..2990b69 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/android/androidDevice.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.android; + +import com.att.nsa.drumlin.app.userAgents.devices.genericDevice; +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; +import com.att.nsa.drumlin.app.userAgents.devices.unknownFixedScreen; + +public class androidDevice extends genericDevice +{ + public androidDevice () + { + super ( new unknownFixedScreen (), true ); + } + + public androidDevice ( screenInfo si ) + { + super ( si, true ); + } + + @Override + public String getOsName () + { + return "Android"; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/computers/macintosh.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/computers/macintosh.java new file mode 100644 index 0000000..9d39a3c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/computers/macintosh.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.computers; + +import com.att.nsa.drumlin.app.userAgents.devices.genericDevice; +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; +import com.att.nsa.drumlin.app.userAgents.devices.unknownFixedScreen; + +public class macintosh extends genericDevice +{ + public macintosh () + { + super ( new unknownFixedScreen (), false ); + } + + public macintosh ( screenInfo si ) + { + super ( si, false ); + } + + @Override + public String getName () + { + return "Apple Macintosh"; + } + + @Override + public String getOsName () + { + return "OS X"; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/device.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/device.java new file mode 100644 index 0000000..b7a200a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/device.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices; + +public interface device +{ + String getName (); + String getVersion (); + + screenInfo getScreenInfo (); + + String getOsName (); + String getOsVersion (); + + boolean isMobile (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/genericDevice.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/genericDevice.java new file mode 100644 index 0000000..fe21e3d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/genericDevice.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices; + +public class genericDevice implements device +{ + public genericDevice () + { + this ( false ); + } + + public genericDevice ( boolean isMobile ) + { + this ( new screenInfo (), isMobile ); + } + + public genericDevice ( screenInfo si, boolean isMobile ) + { + fScreen = si; + fIsMobile = isMobile; + } + + @Override + public String getName () + { + return "generic"; + } + + @Override + public String getVersion () + { + return ""; + } + + @Override + public screenInfo getScreenInfo () + { + return fScreen; + } + + @Override + public String getOsName () + { + return "generic"; + } + + @Override + public String getOsVersion () + { + return ""; + } + + @Override + public boolean isMobile () + { + return fIsMobile; + } + + private final screenInfo fScreen; + private final boolean fIsMobile; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone.java new file mode 100644 index 0000000..f0522fe --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.ios; + +import com.att.nsa.drumlin.app.userAgents.devices.genericDevice; +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; +import com.att.nsa.drumlin.app.userAgents.devices.unknownFixedScreen; + +public class iPhone extends genericDevice +{ + public iPhone () + { + super ( new unknownFixedScreen (), true ); + } + + public iPhone ( screenInfo si ) + { + super ( si, true ); + } + + @Override + public String getName () + { + return "iPhone"; + } + + @Override + public String getOsName () + { + return "iOS"; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone3.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone3.java new file mode 100644 index 0000000..1a969a8 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone3.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.ios; + +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; + +public class iPhone3 extends iPhone +{ + public iPhone3 () + { + super ( new screenInfo ( 320, 480, 163 ) ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone4.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone4.java new file mode 100644 index 0000000..46f3a22 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone4.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.ios; + +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; + +public class iPhone4 extends iPhone +{ + public iPhone4 () + { + super ( new screenInfo ( 640, 960, 326 ) ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone5.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone5.java new file mode 100644 index 0000000..398221c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/ios/iPhone5.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices.ios; + +import com.att.nsa.drumlin.app.userAgents.devices.screenInfo; + +public class iPhone5 extends iPhone +{ + public iPhone5 () + { + super ( new screenInfo ( 640, 1136, 326 ) ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/screenInfo.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/screenInfo.java new file mode 100644 index 0000000..f3d42f7 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/screenInfo.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices; + +public class screenInfo +{ + public screenInfo () + { + this ( -1, -1, -1 ); + } + + public screenInfo ( int width, int height, int dpi ) + { + fWidth = width; + fHeight = height; + fDpi = dpi; + } + + public boolean isFixedSize () { return fWidth != -1 || fHeight != -1; }; + public int getWidth () { return fWidth; } + public int getHeight () { return fHeight; } + public int getDpi () { return fDpi; } + + private final int fWidth; + private final int fHeight; + private final int fDpi; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/unknownFixedScreen.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/unknownFixedScreen.java new file mode 100644 index 0000000..c9b5418 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/devices/unknownFixedScreen.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents.devices; + +public class unknownFixedScreen extends screenInfo +{ + public unknownFixedScreen () + { + super ( -1, -1, -1 ); + } + + @Override + public boolean isFixedSize () { return true; }; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/genericAgent.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/genericAgent.java new file mode 100644 index 0000000..9f2edca --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/genericAgent.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents; + +import com.att.nsa.drumlin.app.userAgents.browsers.browser; +import com.att.nsa.drumlin.app.userAgents.browsers.genericBrowser; +import com.att.nsa.drumlin.app.userAgents.devices.device; +import com.att.nsa.drumlin.app.userAgents.devices.genericDevice; + +/** + * Used when the user agent is a completely generic/unknown system. + * @author peter + */ +public class genericAgent implements userAgent +{ + public genericAgent () + { + this ( new genericDevice(), new genericBrowser () ); + } + + public genericAgent ( device d, browser b ) + { + fDevice = d; + fBrowser = b; + } + + @Override + public String getDeviceName () + { + return fDevice.getName (); + } + + @Override + public String getOsName () + { + return fDevice.getOsName (); + } + + @Override + public String getOsVersion () + { + return fDevice.getOsVersion (); + } + + @Override + public String getBrowserCanonicalName () + { + return fBrowser.getName (); + } + + @Override + public String getBrowserCanonicalVersion () + { + return fBrowser.getVersion (); + } + + @Override + public boolean getIsMobile () + { + return fDevice.isMobile (); + } + + @Override + public boolean getIsFixedScreenSize () + { + return fDevice.getScreenInfo ().isFixedSize (); + } + + @Override + public int getScreenWidth () + { + return fDevice.getScreenInfo ().getWidth (); + } + + @Override + public int getScreenHeight () + { + return fDevice.getScreenInfo ().getHeight (); + } + + @Override + public int getScreenDpi () + { + return fDevice.getScreenInfo ().getDpi (); + } + + private final device fDevice; + private final browser fBrowser; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgent.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgent.java new file mode 100644 index 0000000..57d190a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgent.java @@ -0,0 +1,22 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents; + +public interface userAgent +{ + String getDeviceName (); + String getOsName (); + String getOsVersion (); + + String getBrowserCanonicalName (); + String getBrowserCanonicalVersion (); + + boolean getIsMobile (); + + boolean getIsFixedScreenSize (); + int getScreenWidth (); + int getScreenHeight (); + int getScreenDpi (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentFeature.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentFeature.java new file mode 100644 index 0000000..8806803 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentFeature.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents; + +public class userAgentFeature +{ + public String getName () { return fName; } + public String getVersion () { return fVersion; } + public String getComment () { return fComment; } + + private final String fName; + private final String fVersion; + private final String fComment; + + userAgentFeature ( String name ) + { + this ( name, "", "" ); + } + + userAgentFeature ( String name, String version ) + { + this ( name, version, "" ); + } + + userAgentFeature ( String name, String version, String comment ) + { + fName = name == null ? "" : name; + fVersion = version == null ? "" : version; + fComment = comment == null ? "" : comment; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentInfo.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentInfo.java new file mode 100644 index 0000000..a51e026 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/app/userAgents/userAgentInfo.java @@ -0,0 +1,228 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.app.userAgents; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.rendering.DrumlinRenderContext; +import com.att.nsa.drumlin.till.data.rrConvertor; +import com.att.nsa.drumlin.till.data.rrConvertor.conversionError; +import com.att.nsa.drumlin.till.data.stringUtils; + +/** + * Do some analysis on the user agent. + * @author peter + */ +public class userAgentInfo +{ + public static userAgent populateUserAgentInfo ( DrumlinRequestContext ctx ) + { + final userAgentInfo uai = analyze ( ctx.request () ); + final userAgent ua = deviceDetector.detect ( uai, ctx.systemSettings () ); + + final DrumlinRenderContext rc = ctx.renderer (); + rc.put ( "drumlinUserAgentInfo", ua ); + + return ua; + } + + /** + * Pass a request and get user agent info. + * @param r + * @return a user agent analysis + */ + public static userAgentInfo analyze ( DrumlinRequest r ) + { + final String userAgent = r.getFirstHeader ( "User-Agent" ); + return new userAgentInfo ( userAgent ); + } + + /** + * Construct an analysis for a given user agent string. + * @param userAgent + */ + public userAgentInfo ( String userAgent ) + { + fMap = new HashMap (); + parse ( userAgent ); + } + + /** + * Is a given feature listed in this user agent? + * @param name the name of the feature + * @return true/false + */ + public boolean hasFeature ( String name ) + { + return fMap.containsKey ( name ); + } + + /** + * get the details about a given feature + * @param name + * @return a user agent feature record + */ + public userAgentFeature getFeatureDetails ( String name ) + { + return fMap.get ( name ); + } + + /** + * get the version string for a feature. If it doesn't exist, null is returned. + * @param name + * @return the version string or null + */ + public String getFeatureVersionString ( String name ) + { + final userAgentFeature uaf = fMap.get ( name ); + if ( uaf != null ) + { + return uaf.getVersion (); + } + return null; + } + + /** + * Get the version value for a feature, if possible. If the version string doesn't exist, -1.0 is returned. + * If it exists but can't be parsed as a long, a conversionError is thrown. + * @param name + * @return a numeric value for the feature version + * @throws conversionError + */ + public double getFeatureVersion ( String name ) throws conversionError + { + final String v = getFeatureVersionString ( name ); + if ( v == null ) + { + return -1.0; + } + return rrConvertor.convertToDouble ( v ); + } + + public double getFeatureVersion ( String name, double errValue ) + { + final String v = getFeatureVersionString ( name ); + if ( v == null ) + { + return -1.0; + } + return rrConvertor.convertToDouble ( v, errValue ); + } + + /** + * Get the comment for a feature. If the feature doesn't exist, null is returned. + * @param name + * @return the feature comment string + */ + public String getFeatureComment ( String name ) + { + final userAgentFeature uaf = fMap.get ( name ); + if ( uaf != null ) + { + return uaf.getComment (); + } + return null; + } + + private Map fMap; + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( userAgentInfo.class ); + + private void parse ( String ua ) + { + String currentName = null; + String currentVersion = null; + String currentComment = null; + + // deal with no header as if it's an empty header + if ( ua == null ) ua = ""; + + ua = ua.trim (); + while ( ua.length () > 0 ) + { + if ( ua.startsWith ( "(" ) ) + { + if ( currentName != null ) + { + String commentPart = ""; + final int closeParen = ua.indexOf ( ')' ); + if ( closeParen >= 0 ) + { + commentPart = ua.substring ( 0, closeParen ); + ua = ua.substring ( closeParen + 1 ).trim (); + } + else + { + // bad format. use it all and warn + commentPart = ua.substring ( 1 ); + ua = ""; + log.warn ( "Missing a close paren in a user agent comment for " + currentName + "." ); + } + currentComment = ( currentComment == null ? commentPart : currentComment + commentPart ); + } + else + { + // stray comment field + log.warn ( "Found a comment section without a preceding feature name. Ignoring." ); + } + } + else + { + // store the last entry + if ( currentName != null ) + { + final userAgentFeature uaf = new userAgentFeature ( currentName, currentVersion, currentComment ); + fMap.put ( currentName, uaf ); + currentName = null; + currentVersion = null; + currentComment = null; + } + + String token = ua; + final int space = stringUtils.indexOf ( ua, new stringUtils.charSelector() + { + @Override + public boolean select ( Character c ) + { + return Character.isWhitespace ( c ); + } + } ); + if ( space >= 0 ) + { + token = ua.substring ( 0, space ); + ua = ua.substring ( space ).trim (); + } + else + { + // take it all + ua = ""; + } + + final int slash = token.indexOf ( '/' ); + if ( slash < 0 ) + { + currentName = token; + currentVersion = ""; + } + else + { + currentName = token.substring ( 0, slash ); + currentVersion = token.substring ( slash + 1 ); + } + } + } + + // store the last entry + if ( currentName != null ) + { + final userAgentFeature uaf = new userAgentFeature ( currentName, currentVersion, currentComment ); + fMap.put ( currentName, uaf ); + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnection.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnection.java new file mode 100644 index 0000000..9ba6816 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnection.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework; + +import java.util.HashMap; + +import javax.servlet.ServletException; + +/** + * The DrumlinConnection represents a session between a client system and the server. + * + * @author peter@rathravane.com + */ +public interface DrumlinConnection +{ + /** + * Called when the servlet associates this connection to a client system. + * @param ws + * @param dcc + * @throws ServletException + */ + void onSessionCreate ( DrumlinServlet ws, DrumlinConnectionContext dcc ) throws ServletException; + + /** + * Called when the connection is closing. + */ + void onSessionClose (); + + /** + * Called when the session receives client activity. + */ + void noteActivity (); + + /** + * Called when the servlet requires the connection to build a context for use by + * the Velocity renderer. + * @param context + */ + void buildTemplateContext ( HashMap context ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnectionContext.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnectionContext.java new file mode 100644 index 0000000..5b6172b --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinConnectionContext.java @@ -0,0 +1,23 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework; + +import java.util.concurrent.TimeUnit; + +/** + * Context provided to a DrumlinConnection when the servlet associates a client call. + * + * @author peter@rathravane.com + */ +public interface DrumlinConnectionContext +{ + /** + * If the connection should timeout after inactivity, call setInactiveExpiration on + * the connection after it's setup. + * @param units + * @param tu + */ + void setInactiveExpiration ( long units, TimeUnit tu ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinErrorHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinErrorHandler.java new file mode 100644 index 0000000..0e80ea6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinErrorHandler.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * You can register an error handler with the request router. + */ +public interface DrumlinErrorHandler +{ + /** + * Handle the error. Do not throw out of this method! + * @param ctx + * @param cause + */ + void handle ( DrumlinRequestContext ctx, Throwable cause ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinRuntimeControls.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinRuntimeControls.java new file mode 100644 index 0000000..11f425c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinRuntimeControls.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework; + +import java.util.Collection; +import java.util.Map; + +import com.att.nsa.drumlin.till.nv.impl.nvBaseReadable; +import com.att.nsa.drumlin.till.nv.impl.nvWriteableTable; + +/** + * The runtime controls are read like regular settings in the system, but are not + * expected to be cached. + * + * @author peter + * + */ +public class DrumlinRuntimeControls extends nvBaseReadable +{ + public static final String kSetting_LogHeaders = "drumlin.logging.requestHeaders"; + + public DrumlinRuntimeControls () + { + fTable = new nvWriteableTable (); + } + + public void setLogHeaders ( boolean b ) + { + fTable.set ( kSetting_LogHeaders, b ); + } + + @Override + public int size () + { + return fTable.size(); + } + + @Override + public Collection getAllKeys () + { + return fTable.getAllKeys(); + } + + @Override + public Map getCopyAsMap () + { + return fTable.getCopyAsMap(); + } + + @Override + public boolean hasValueFor ( String key ) + { + return fTable.hasValueFor( key ); + } + + @Override + public String getString ( String key ) + throws missingReqdSetting + { + return fTable.getString ( key ); + } + + private final nvWriteableTable fTable; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServlet.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServlet.java new file mode 100644 index 0000000..a4d7e86 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServlet.java @@ -0,0 +1,982 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework; + +import java.io.File; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLDecoder; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.app.event.EventCartridge; +import org.apache.velocity.app.event.implement.IncludeRelativePath; +import org.apache.velocity.runtime.RuntimeConstants; +import org.slf4j.LoggerFactory; + +import com.att.nsa.clock.SaClock; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.rendering.vtlTools.DrumlinVtlHelper; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter.noMatchingRoute; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteInvocation; +import com.att.nsa.drumlin.service.standards.HttpStatusCodes; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.impl.nvInstallTypeWrapper; +import com.att.nsa.drumlin.till.nv.impl.nvPropertiesFile; +import com.att.nsa.drumlin.till.nv.impl.nvReadableStack; +import com.att.nsa.drumlin.util.rrVeloLogBridge; +import com.att.nsa.logging.LoggingContext; +import com.att.nsa.logging.LoggingContextFactory; +import com.att.nsa.logging.log4j.EcompFields; + +/** + * The base servlet associates a connection object with an HTTP connection. Even + * session-less servers like a RESTful API have connections -- they're just not + * stored across calls. + * + * @author peter + */ +public class DrumlinServlet extends HttpServlet +{ + public static final String kJvmSetting_FileRoot = "RRWT_FILES"; + public static final String kSetting_BaseWebAppDir = "drumlin.webapp.base"; + + // in your HTML templates, use "$warRoot" to mean the base of your war file/directory + public static final String kWarRoot = "warRoot"; + + // in your HTML templates, use "$servletRoot" to mean the path that this servlet is mapped onto + // (your mapping is specified in web.xml, so you could just hardcode it and not use servletRoot + // and instead use $warRoot//) + public static final String kServletRoot = "servletRoot"; + + // a helper tool for velocity + public static final String kDrumlinVtlToolName = "drumlinVtl"; + + /** + * Session life cycle is determined at servlet creation time. + * @author peter@rathravane.com + */ + public enum sessionLifeCycle + { + /** + * No session data is stored on the server for the client. + */ + NO_SESSION, + + /** + * The server stores a "flash session" for the client, allowing your code to + * retrieve data that was stored while processing the last request from each client only. + */ + FLASH_SESSION, + + /** + * The server stores a full session for the client. The session eventually + * expires if not explicitly removed. + */ + FULL_SESSION + }; + + /** + * Construct a servlet with default settings and the "no session" session life cycle. + */ + public DrumlinServlet () + { + this ( sessionLifeCycle.NO_SESSION ); + } + + /** + * Construct a servlet with default settings, and the specified session life cycle. + */ + public DrumlinServlet ( String prefsFileName ) + { + this ( prefsFileName, sessionLifeCycle.NO_SESSION ); + } + + /** + * Construct a servlet with default settings, and the specified session life cycle. + */ + public DrumlinServlet ( sessionLifeCycle slc ) + { + this ( null, slc ); + } + + /** + * Construct a servlet with settings from a named file, and the specified + * session life cycle. + * + * @param prefsFileName + * @param slc + */ + public DrumlinServlet ( String prefsFileName, sessionLifeCycle slc ) + { + this ( null, prefsFileName, slc ); + } + + /** + * Construct a servlet with settings from a named file, and the specified + * session life cycle. + * + * @param prefsFileName + * @param slc + */ + public DrumlinServlet ( rrNvReadable settings, String addlSettingsFileName, sessionLifeCycle slc ) + { + fWebInfDir = null; + fProvidedPrefs = settings; + fPrefsConfigFilename = addlSettingsFileName; + fSessionLifeCycle = slc; + fRouter = null; + fObjects = new HashMap (); + fSearchDirs = new LinkedList (); + fRuntimeControls = new DrumlinRuntimeControls (); + fBasePath = "./"; + + // setup a logging context + fLogContext = new LoggingContextFactory.Builder().build(); + } + + /** + * Initialize the servlet (called by the servlet container). + */ + @Override + public final void init ( ServletConfig sc ) throws ServletException + { + super.init ( sc ); + + // find the WEB-INF dir + fBasePath = sc.getServletContext ().getRealPath ( "/" ); + if ( fBasePath == null ) + { + log.info ( "Servlet engine did not map '/' into a real path. Using './'" ); + fBasePath = "./"; + } + + log.info ( "working dir = " + System.getProperty("user.dir") ); + log.info ( "servlet class: " + this.getClass ().getName() ); + log.info ( "real path of '/' = " + fBasePath ); + + // make the settings + final nvReadableStack settingsStack = new nvReadableStack (); +// settingsStack.push ( new nvJvmProperties () ); + settingsStack.push ( new DrumlinServletSettings ( sc ) ); + if ( fProvidedPrefs != null ) + { + settingsStack.push ( fProvidedPrefs ); + } + + // find the base webapp directory, normally "WEB-INF" + final String webappDirName = settingsStack.getString ( kSetting_BaseWebAppDir, new File ( fBasePath, "WEB-INF" ).getAbsolutePath () ); + final File checkDir = new File ( webappDirName ); + if ( checkDir.exists () ) + { + fWebInfDir = checkDir; + } + else + { + log.debug ( "Drumlin can't find the webapp's base directory. Used '" + webappDirName + "'." ); + } + + // get additional search dirs + final String searchDirString = settingsStack.getString ( "drumlin.config.search.dirs", null ); + if ( searchDirString != null ) + { + log.info ( "config search dirs: " + searchDirString ); + final String searchDirs[] = searchDirString.split ( ":" ); + for ( String searchDir : searchDirs ) + { + addToFileSearchDirs ( new File ( searchDir ) ); + } + } + else + { + log.info ( "drumlin.config.search.dirs is not set. (Typically set in web.xml)" ); + } + + if ( fPrefsConfigFilename != null && fPrefsConfigFilename.length() > 0 ) + { + try + { + log.info ( "finding config stream named [" + fPrefsConfigFilename + "]." ); + final URL configFile = findStream ( fPrefsConfigFilename ); + if ( configFile != null ) + { + log.info ( "chose stream [" + configFile.toString () + "]." ); + final rrNvReadable filePrefs = new nvPropertiesFile ( configFile ); + settingsStack.push ( filePrefs ); + } + else + { + log.warn ( "could not find config stream." ); + } + } + catch ( rrNvReadable.loadException e ) + { + log.warn ( "Couldn't load settings from [" + fPrefsConfigFilename + "]." ); + } + } + else + { + log.info ( "no preferences file specified to " + getClass().getSimpleName() + "'s constructor." ); + } + + // add the runtime control settings to the settings stack + settingsStack.push ( fRuntimeControls ); + + // put a wrapper on the top-level settings object to allow for + // installation-type specific settings. + final rrNvReadable appLevelSettings = makeSettings ( settingsStack ); + fSettings = new nvInstallTypeWrapper ( appLevelSettings ); + + // routing setup + fRouter = new DrumlinRequestRouter (); + + // velocity setup + try + { + fVelocity = new VelocityEngine (); + fVelocity.setProperty ( VelocityEngine.RUNTIME_LOG_LOGSYSTEM, new rrVeloLogBridge ( log ) ); + + setupResourceLoader ( fVelocity, fSettings ); + + fVelocity.init (); + + // create a base context and add the servletRoot value + fBaseContext = new VelocityContext (); + + // we want relative template finding + { + final EventCartridge ec = new EventCartridge (); + ec.addEventHandler ( new IncludeRelativePath () ); + ec.attachToContext ( fBaseContext ); + } + + // contextPath is the base directory for this servlet. if the servlet is at the + // root location, the context path is "", per spec. + final String contextPath = sc.getServletContext ().getContextPath (); + log.info ( "context path ($" + kWarRoot + "): [" + contextPath + "]." ); + fBaseContext.put ( kWarRoot, contextPath ); + fBaseContext.put ( kServletRoot, contextPath ); + + // some simple tools for velocity + fBaseContext.put ( kDrumlinVtlToolName, new DrumlinVtlHelper () ); + } + catch ( Exception e ) + { + throw new ServletException ( e ); + } + + // app-level setup + try + { + log.info ( "Calling app servlet setup." ); + servletSetup (); + } + catch ( rrNvReadable.missingReqdSetting e ) + { + log.error ( "Shutting down due to missing setting. " + e.getMessage () ); + throw new ServletException ( e ); + } + catch ( rrNvReadable.invalidSettingValue e ) + { + log.error ( "Shutting down due to invalid setting. " + e.getMessage () ); + throw new ServletException ( e ); + } + + log.info ( "Servlet is ready." ); + } + + @Override + public final void destroy () + { + super.destroy (); + try + { + servletShutdown (); + } + catch ( Exception x ) + { + log.error ( "During tear-down: " + x.getMessage () ); + } + } + + /** + * Find the named resource and return an InputStream for it. This is related to findFile(), but + * is built to be more general. Use this if you don't actually require a file on disk.
+ *
+ * 1. If the JVM system properties include a setting with the key specified by kJvmSetting_FileRoot, look + * for the file relative to that path.
+ * 2. Try the system's findResource() call. + * 3. Try findFile() + *
+ * @param resourceName + * @return an InputStream, or null + */ + public static URL findStream ( String resourceName, Class clazz ) + { + try + { + // first try it as an absolute file name + File file = new File ( resourceName ); + if ( file.isAbsolute () ) + { + return file.toURI().toURL(); + } + + // next try the file root setting, which takes precedence + final String filesRoot = System.getProperty ( kJvmSetting_FileRoot, null ); + if ( filesRoot != null ) + { + final String fullPath = filesRoot + "/" + resourceName; + log.debug ( "Looking for [" + fullPath + "]." ); + file = new File ( fullPath ); + if ( file.exists () ) + { + return file.toURI().toURL(); + } + } + + // next try the class's resource finder + URL res = clazz.getClassLoader().getResource ( resourceName ); + if ( res != null ) + { + return res; + } + + // now try the system class loaders' resource finder + res = ClassLoader.getSystemResource ( resourceName ); + if ( res != null ) + { + return res; + } + } + catch ( MalformedURLException e ) + { + log.warn ( "Unexpected failure to convert a local filename into a URL: " + e.getMessage () ); + } + + return null; + } + + public URL findStream ( String resourceName ) + { + URL res = findStream ( resourceName, getClass() ); + if ( res != null ) return res; + + try + { + // finally, do the regular file search + final File f = findFile ( resourceName ); + if ( f.exists () ) + { + final URI u = f.toURI (); + final URL uu = u.toURL (); + return uu; + } + } + catch ( MalformedURLException e ) + { + log.warn ( "Unexpected failure to convert a local filename into a URL: " + e.getMessage () ); + } + + return null; + } + + /** + * Find a file given a file name. If the name is absolute, the file is returned. Otherwise, + * the file is located using this search path:
+ *
+ * 1. If the JVM system properties include a setting with the key specified by kJvmSetting_FileRoot, look + * for the file relative to that path.
+ * 2. If not yet found, look for the file relative to the servlet's WEB-INF directory, if that exists.
+ * 3. If not yet found, look for the file relative to the servlet's real path for "/". (Normally inside the war.)
+ * 4. If not yet found, check each app-provided search directory.
+ * 4. If not yet found, return a File with the relative path as-is. (This does not mean the file exists!) + * + * @param appRelativePath + * @return a File + */ + public File findFile ( String appRelativePath ) + { + File file = new File ( appRelativePath ); + if ( !file.isAbsolute () ) + { + final String filesRoot = System.getProperty ( kJvmSetting_FileRoot, null ); + if ( filesRoot != null ) + { + final String fullPath = filesRoot + "/" + appRelativePath; + log.debug ( "Looking for [" + fullPath + "]." ); + file = new File ( fullPath ); + } + + // check in WEB-INF (FIXME: using a member variable; think about thread synchronization) + if ( !file.exists () && fWebInfDir != null ) + { + file = new File ( fWebInfDir, appRelativePath ); + log.debug ( "Looking for [" + file.getAbsolutePath() + "]." ); + } + + // check in webapp's "/" + if ( !file.exists () ) + { + final String fullPath = fBasePath + ( fBasePath.endsWith ( "/" ) ? "" : "/" ) + appRelativePath; + log.debug ( "Looking for [" + fullPath + "]." ); + file = new File ( fullPath ); + } + + // check search dirs specified by app + if ( !file.exists () ) + { + for ( File dir : fSearchDirs ) + { + final File candidate = new File ( dir, appRelativePath ); + log.debug ( "Looking for [" + candidate.getAbsolutePath () + "]." ); + if ( candidate.exists () ) + { + file = candidate; + break; + } + } + } + + if ( !file.exists () ) + { + file = new File ( appRelativePath ); + } + } + log.debug ( "Given [" + appRelativePath + "], using file [" + file.getAbsolutePath () + "]." ); + return file; + } + + /** + * Get settings in use by this servlet. They can come from the servlet container, from an + * optional config file named by the string provided to the constructor, and anything else + * the servlet init code (in the concrete class) decides to add. + * + * @return settings + */ + public rrNvReadable getSettings () + { + return fSettings; + } + + /** + * Get servlet controls. (These are settings that don't get cached.) + * @return + */ + public DrumlinRuntimeControls getControls () + { + return fRuntimeControls; + } + + /** + * Put an object into the servlet's directory by name. + * @param key + * @param o + */ + public void putObject ( String key, Object o ) + { + fObjects.put ( key, o ); + } + + /** + * Get an object from the servlet's directory by name. If none is found, null is returned. + * @param key + * @return a previously stored object, or null. + */ + public Object getObject ( String key ) + { + return fObjects.get ( key ); + } + + /** + * Get the velocity engine. + * @return the Velocity engine + */ + public VelocityEngine getVelocity () + { + return fVelocity; + } + + /** + * Get the base context for Velocity. This context is shared among all sessions. + * + * @return a velocity context + */ + public VelocityContext getBaseContext () + { + return fBaseContext; + } + + /** + * Add an object to the base velocity context. Keep in mind that the base velocity + * context is shared among all sessions. + * + * @param key + * @param o + */ + public void addToBaseContext ( String key, Object o ) + { + if ( fBaseContext == null ) + { + log.warn ( "Call to addToBaseContext is ignored. You need to init the servlet first." ); + return; + } + fBaseContext.put ( key, o ); + } + + /** + * Create a session. + * @return a session. + * @throws rrNvReadable.missingReqdSetting + */ + public DrumlinConnection createSession () throws rrNvReadable.missingReqdSetting + { + return null; + } + + /** + * Get the servlet's request router. + * @return a request router. + */ + public DrumlinRequestRouter getRequestRouter () + { + return fRouter; + } + + /** + * Get the servlet's base URL. + * @return the $servletRoot value + */ + public String getBaseUrl () + { + return fBaseContext.get ( kServletRoot ).toString (); + } + + /** + * Get the shared logging context for this servlet. Anything written to this logging + * context will be copied into each thread's logging context when the thread calls + * getLoggingContextForThread() + * + * @return a common logging context + */ + public LoggingContext getCommonLoggingContext () + { + return fLogContext; + } + + /** + * Get a logging context for the current thread that's based on the common logging context. + * @return a logging context for the current thread + */ + public LoggingContext getLoggingContextForThread () + { + // note that this operation requires everything from the common context + // to be (re)copied into the target context. That seems slow, but it actually + // helps prevent the thread from overwriting supposedly common data. It also + // should be fairly quick compared with the overhead of handling the actual + // service call. + + return new LoggingContextFactory.Builder(). + withBaseContext ( getCommonLoggingContext () ). + build(); + } + + /** + * Override this to take the settings built by the base servlet and return + * something wrapping them (or different, even) + * @param fromBase + * @return a settings object + */ + protected rrNvReadable makeSettings ( rrNvReadable fromBase ) + { + return fromBase; + } + + /** + * Add a directory to the file search directory path. + * @param dir + */ + protected synchronized void addToFileSearchDirs ( File dir ) + { + if ( dir.exists() && dir.isDirectory () ) + { + fSearchDirs.add ( dir ); + } + else + { + log.warn ( "File [" + dir.toString () + "] is not a directory. Ignored." ); + } + } + + /** + * Called at the end of servlet initialization. Override servletSetup to do + * custom init work in your servlet. + * + * @throws drumlinSettings.missingReqdSetting + * @throws drumlinSettings.invalidSettingValue + * @throws ServletException + */ + protected void servletSetup () throws rrNvReadable.missingReqdSetting, rrNvReadable.invalidSettingValue, ServletException {} + + /** + * override servletShutdown to do custom shutdown work in your servlet. Note that this isn't always called, + * depending on the servlet container. + */ + protected void servletShutdown () {} + + @Override + protected final void service ( HttpServletRequest req, HttpServletResponse resp ) throws ServletException, java.io.IOException + { + String logLine = null; + final long startMs = System.currentTimeMillis (); + + final LoggingContext lc = getLoggingContextForThread(); + + try + { + populateLoggingContextForThread ( lc, req ); + + String methodForLog = req.getMethod(); + if ( methodForLog.length() > 3 ) methodForLog = methodForLog.substring(0,3); + + // build an initial log line + logLine = req.getRemoteHost () + ":" + req.getRemotePort() + " " + methodForLog + " " + req.getRequestURI (); + final String queryString = req.getQueryString (); + if ( queryString != null ) + { + logLine = logLine + "?" + queryString; + } + log.debug ( logLine ); + + // update the servlet root + { + final String uri = URLDecoder.decode ( req.getRequestURI (), "UTF-8" ); + final int uriLen = uri.length (); + + final String path = req.getPathInfo (); + final int pathLen = ( path == null ? 0 : path.length () ); + + final int endIndex = ( path == null ) ? 0 : uriLen - pathLen + 1; + // FIXME: check endindex! + + String basePart = uri.substring ( 0, endIndex ); + if ( basePart.endsWith ( "/" ) ) + { + basePart = basePart.substring ( 0, basePart.length () - 1 ); + } + + final String srIs = fBaseContext.get ( kServletRoot ).toString (); + if ( !srIs.equals ( basePart ) ) + { + log.info ( "updating $" + kServletRoot + "=" + basePart + " (was " + srIs + ")" ); + fBaseContext.put ( kServletRoot, basePart ); + } + } + + if ( getSettings().getBoolean ( DrumlinRuntimeControls.kSetting_LogHeaders, false ) ) + { + log.info ( "--" ); + log.info ( "REQUEST from " + req.getRemoteHost () + " (" + req.getRemoteAddr () + "):" ); + log.info ( " " + req.getMethod () + " " + req.getPathInfo () + " " + req.getQueryString () ); + log.info ( "" ); + + final Enumeration e = req.getHeaderNames (); + while ( e.hasMoreElements () ) + { + final String name = e.nextElement ().toString (); + final String val = req.getHeader ( name ); + log.info ( " " + name + ": " + val ); + } + log.info ( "--" ); + } + + String routeName = "unknownRoute"; + + final DrumlinRequestContext ctx = createHandlingContext ( req, resp, getSession ( req ), fObjects, fRouter ); + try + { + final DrumlinRouteInvocation handler = fRouter.route ( ctx.request () ); + routeName = handler.getName (); + handler.run ( ctx ); + } + catch ( noMatchingRoute e ) + { + onError ( ctx, e, new DrumlinErrorHandler () + { + @Override + public void handle ( DrumlinRequestContext ctx, Throwable cause ) + { + ctx.response ().sendError ( HttpStatusCodes.k404_notFound, "Not found." ); + } + } ); + } + catch ( InvocationTargetException x ) + { + final Throwable t = x.getCause (); + if ( t != null ) + { + onError ( ctx, t, null ); + } + else + { + onError ( ctx, x, null ); + } + } + catch ( Throwable t ) + { + onError ( ctx, t, null ); + } + + final long endMs = System.currentTimeMillis (); + final long durationMs = Math.max ( 0, endMs - startMs ); + onRouteComplete ( routeName, durationMs ); + lc.put ( "timer", durationMs ); + + // more ecomp log population + final int statusCode = ctx.response ().getStatusCode (); + lc.put ( "statusCode", statusCode >= 500 ? "ERROR":"COMPLETE" ); + switch ( statusCode ) + { + case HttpStatusCodes.k401_unauthorized: + lc.put ( "responseCode", 100 ); + break; + case HttpStatusCodes.k503_serviceUnavailable: + lc.put ( "responseCode", 200 ); + break; + case HttpStatusCodes.k200_ok: + case HttpStatusCodes.k201_created: + case HttpStatusCodes.k202_accepted: + case HttpStatusCodes.k203_nonAuthoritativeInformation: + case HttpStatusCodes.k204_noContent: + case HttpStatusCodes.k205_resetContent: + case HttpStatusCodes.k206_partialContent: + lc.put ( "responseCode", 0 ); // there's no success category... ?? + break; + + default: + // 300-599 + lc.put ( "responseCode", 900 + (statusCode-300) ); + } + lc.put ( "responseDescription", "HTTP " + statusCode ); + + logLine = logLine + " " + statusCode + " " + durationMs + " ms"; + } + finally + { + if ( logLine != null ) log.info ( logLine ); + } + } + + /** + * Override this to provide logging context. This implementation writes requestId, + * serviceInstanceId, and ipAddress + * + * @param lc + * @param req + */ + protected void populateLoggingContextForThread ( LoggingContext lc, HttpServletRequest req ) + { + // We need a "unique" value for the request ID, so we use a UUID (that helps with + // keeping the ID unique across servers) + lc.put ( "requestId", UUID.randomUUID ().toString () ); + + // service name is the API invoked + lc.put ( "serviceName", req.getPathInfo () ); + + // timing + lc.put ( EcompFields.kBeginTimestampMs, SaClock.now () ); + + // result + lc.put ( "statusCode", "" ); + lc.put ( "responseCode", "" ); + + // remote partner/IP + final String remoteIp = req.getRemoteAddr (); + if ( remoteIp != null ) + { + lc.put ( "ipAddress", remoteIp ); + lc.put ( "clientIpAddress", remoteIp ); + lc.put ( "partnerName", remoteIp ); + } + } + + /** + * Override this to create a custom handling context for your request handlers. + * @param req + * @param resp + * @param dc + * @param objects + * @param rr + * @return + */ + protected DrumlinRequestContext createHandlingContext ( HttpServletRequest req, HttpServletResponse resp, DrumlinConnection dc, HashMap objects, DrumlinRequestRouter rr ) + { + return new DrumlinRequestContext ( this, req, resp, dc, objects, rr ); + } + + protected void onRouteComplete ( String name, long durationMs ) {} + + private void onError ( DrumlinRequestContext ctx, Throwable t, DrumlinErrorHandler defHandler ) + { + DrumlinErrorHandler eh = fRouter.route ( t ); + if ( eh == null && defHandler != null ) + { + eh = defHandler; + } + + if ( eh != null ) + { + try + { + eh.handle ( ctx, t ); + } + catch ( Throwable tt ) + { + log.warn ( "Error handler failed, handling a " + t.getClass().getName() + ", with " + tt.getMessage () ); + ctx.response ().sendError ( HttpStatusCodes.k500_internalServerError, t.getMessage () ); + } + } + else + { + log.warn ( "No handler defined for " + t.getClass().getName() + ". Sending 500." ); + ctx.response ().sendError ( HttpStatusCodes.k500_internalServerError, t.getMessage () ); + + final StringWriter sw = new StringWriter (); + final PrintWriter pw = new PrintWriter ( sw ); + t.printStackTrace ( pw ); + pw.close (); + log.warn ( sw.toString () ); + } + } + + private DrumlinConnection getSession ( HttpServletRequest req ) throws ServletException + { + DrumlinConnection result = null; + if ( !fSessionLifeCycle.equals ( sessionLifeCycle.NO_SESSION ) ) + { + try + { + final String servletSessionName = getSessionObjectName ( this.getClass () ); + final String flashSessionName = getFlashSessionObjectName ( this.getClass () ); + + final HttpSession session = req.getSession ( true ); + + // is there a prior flash session? + final DrumlinConnection oldFlashSession = (DrumlinConnection) session.getAttribute ( flashSessionName ); + if ( oldFlashSession != null ) + { + oldFlashSession.onSessionClose (); + } + + // locate the last session + result = (DrumlinConnection) session.getAttribute ( servletSessionName ); + + // if we're using flash sessions, save this session as the last flash session + // and create a new session + if ( fSessionLifeCycle.equals ( sessionLifeCycle.FLASH_SESSION ) ) + { + session.setAttribute ( flashSessionName, result ); + result = null; + } + + if ( result == null ) + { + result = createSession (); + if ( result != null ) + { + session.setAttribute ( servletSessionName, result ); + result.onSessionCreate ( this, new DrumlinConnectionContext () + { + @Override + public void setInactiveExpiration ( long units, TimeUnit tu ) + { + final long timeInSeconds = TimeUnit.SECONDS.convert ( units, tu ); + if ( timeInSeconds < 0 || timeInSeconds > Integer.MAX_VALUE ) + { + throw new IllegalArgumentException ( "Invalid time specification." ); + } + final int timeInSecondsInt = (int) timeInSeconds; + session.setMaxInactiveInterval ( timeInSecondsInt ); + } + } ); + } + } + + if ( result != null ) + { + result.noteActivity (); + } + } + catch ( rrNvReadable.missingReqdSetting e ) + { + throw new ServletException ( e ); + } + } + return result; + } + + private static String getSessionObjectName ( Class c ) + { + return kWebSessionObject + c.getName (); + } + + private static String getFlashSessionObjectName ( Class c ) + { + return kFlashSessionObject + c.getName (); + } + + private rrNvReadable fSettings; + private final DrumlinRuntimeControls fRuntimeControls; + private String fBasePath; + private File fWebInfDir; + private final rrNvReadable fProvidedPrefs; + private final String fPrefsConfigFilename; + private final LinkedList fSearchDirs; + private final sessionLifeCycle fSessionLifeCycle; + private DrumlinRequestRouter fRouter; + private VelocityEngine fVelocity; + private VelocityContext fBaseContext; + private final HashMap fObjects; + private final LoggingContext fLogContext; + + private static final String kWebSessionObject = "drumlin.session."; + private static final String kFlashSessionObject = "drumlin.flash."; + + public static final String kSetting_BaseTemplateDir = "drumlin.templates.path"; + private static final long serialVersionUID = 1L; + private static org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinServlet.class ); + + protected void setupResourceLoader ( VelocityEngine ve, rrNvReadable p ) + { + final String baseTemplateDir = p.getString ( kSetting_BaseTemplateDir, "WEB-INF/templates" ); + final File realDir = findFile ( baseTemplateDir ); + log.info ( "INIT: velocity templates: " + realDir.getAbsolutePath () ); + final boolean caching = Boolean.parseBoolean ( System.getProperty ( "drumlin.cacheTemplates", "true" ) ); + + ve.setProperty ( RuntimeConstants.RESOURCE_LOADER, "file, class" ); + + ve.setProperty ( RuntimeConstants.FILE_RESOURCE_LOADER_PATH, realDir.getAbsolutePath () ); + ve.setProperty ( "file.resource.loader.cache", caching ); + + ve.setProperty ( "class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServletSettings.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServletSettings.java new file mode 100644 index 0000000..294b469 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/DrumlinServletSettings.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.framework; + +import java.util.Enumeration; +import java.util.HashMap; + +import javax.servlet.ServletConfig; + +import com.att.nsa.drumlin.till.nv.impl.nvReadableTable; + +/** + * Wraps a ServletConfig in the settings class used throughout the Drumlin + * framework. + */ +public class DrumlinServletSettings extends nvReadableTable +{ + public DrumlinServletSettings ( ServletConfig sc ) + { + super (); + + final HashMap loaded = new HashMap (); + + final Enumeration e = sc.getInitParameterNames (); + while ( e.hasMoreElements () ) + { + final String name = e.nextElement (); + final String val = sc.getInitParameter ( name ); + loaded.put ( name, val ); + } + + set ( loaded ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequest.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequest.java new file mode 100644 index 0000000..2309e85 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequest.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * A request made to a servlet. + * + * @author peter + */ +public interface DrumlinRequest +{ + /** + * Get the request's URL + * @return the URL + */ + String getUrl (); + + /** + * Get the query string from the HTTP request, or null if there's no query + * @return the query string or null + */ + String getQueryString (); + + /** + * Get the HTTP method for this request. + * @return the HTTP method + */ + String getMethod (); + + /** + * get the request's path within the servlet context + * @return the request path within the servlet context + */ + String getPathInContext (); + + /** + * Get the first value (of 1 or more) for a given header name. + * @param header + * @return null if the header does not exist, or the first value otherwise + */ + String getFirstHeader ( String header ); + + /** + * Get all values for a given header. + * @param header + * @return a list of 0 or more values + */ + List getHeader ( String header ); + + /** + * Get the parameter map for this request. + * @return a map of name/value pairs. + */ + Map getParameterMap (); + + /** + * get a parameter by name + * @param key + * @return null, or the value of the named parameter + */ + String getParameter ( String key ); + + /** + * get a parameter by name. If the parameter does not exist on this request, + * return the default value provided. + * + * @param key + * @param defVal + * @return the value of the parameter, or the default value + */ + String getParameter ( String key, String defVal ); + + /** + * Get a parameter as an integer. + * @param key + * @param defVal + * @return + */ + int getIntParameter ( String key, int defVal ); + + /** + * Change the value of a parameter on this request. (Generally used by validators.) + * @param fieldName + * @param defVal + */ + void changeParameter ( String fieldName, String defVal ); + + /** + * Get the content type header for this request. + * @return + */ + String getContentType (); + + /** + * Get the content length for this request. + * @return the number of bytes of content in this request + */ + int getContentLength (); + + /** + * get the content as an input stream + * @return the body of the request as an input stream + * @throws IOException + */ + InputStream getBodyStream () throws IOException; + + /** + * get the content of the request as text. + * @return a buffered reader on the content of this request. + * @throws IOException + */ + BufferedReader getBodyStreamAsText () throws IOException; + + /** + * get the address of the requesting agent + * @return the address of the requesting agent + */ + String getRemoteAddress (); + + /** + * return true if the request (and response) was made over a secure transport + * @return true if the request was made over a secure transport + */ + boolean isSecure (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequestContext.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequestContext.java new file mode 100644 index 0000000..b966eb5 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinRequestContext.java @@ -0,0 +1,161 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.velocity.VelocityContext; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.DrumlinConnection; +import com.att.nsa.drumlin.service.framework.DrumlinServlet; +import com.att.nsa.drumlin.service.framework.rendering.DrumlinRenderContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * The DrumlinRequestContext provides the servlet, the inbound HTTP request, the outbound + * HTTP response, the Connection, and other information to the request handlers provided by + * the web application. + * + * @author peter@rathravane.com + * + */ +public class DrumlinRequestContext +{ + public DrumlinRequestContext ( DrumlinServlet webServlet, HttpServletRequest req, HttpServletResponse resp, + DrumlinConnection s, Map objects, DrumlinRequestRouter router ) + { + fRequest = req; + fResponse = resp; + fSession = s; + fServlet = webServlet; + fBaseRenderContext = webServlet.getBaseContext (); + fLocalContext = new HashMap (); + fObjects = objects; + fRouter = router; + + fRequestImpl = new StdRequest ( fRequest ); + } + + /** + * Get the connection being serviced. + * @return a connection + */ + public DrumlinConnection session () + { + return fSession; + } + + public rrNvReadable systemSettings () + { + return fServlet.getSettings (); + } + + public DrumlinRequestRouter router () + { + return fRouter; + } + + public DrumlinServlet getServlet () + { + return fServlet; + } + + public String servletPathToFullUrl ( String contentUrl ) + { + final StringBuffer url = new StringBuffer (); + + final String scheme = fRequest.getScheme ().toLowerCase (); + url.append ( scheme ); + url.append ( "://" ); + url.append ( fRequest.getServerName () ); + + final int serverPort = fRequest.getServerPort (); + if ( !( ( scheme.equals ( "http" ) && serverPort == 80 ) || + ( scheme.equals ( "https" ) && serverPort == 443 ) ) ) + { + url.append ( ":" ); + url.append ( serverPort ); + } + + final String path = servletPathToFullPath ( contentUrl ); + url.append ( path ); + + log.info ( "calculated full URL for [" + contentUrl + "]: [" + url + "]" ); + return url.toString (); + } + + public String servletPathToFullPath ( String contentUrl ) + { + return servletPathToFullPath ( contentUrl, fRequest ); + } + + public static String servletPathToFullPath ( String contentUrl, HttpServletRequest req ) + { + final StringBuffer sb = new StringBuffer (); + + final String contextPart = req.getContextPath (); + sb.append ( contextPart ); + + final String servletPart = req.getServletPath (); + sb.append ( servletPart ); + + sb.append ( contentUrl ); + + log.info ( "calculated full path for [" + contentUrl + "]: context=[" + contextPart + "], servlet=[" + + servletPart + "], result=[" + sb.toString () + "]" ); + return sb.toString (); + } + + public Object object ( String key ) + { + return fObjects.get ( key ); + } + + public DrumlinRequest request () + { + return fRequestImpl; + } + + public DrumlinResponse response () + { + return new StdResponse ( fRequest, fResponse, fRouter ); + } + + public DrumlinRenderContext renderer () + { + return new StdRenderer (this); + } + + public void merge ( String templateName, Map data, PrintWriter to ) + { + final VelocityContext ctx = new VelocityContext ( fBaseRenderContext ); + for ( Entry e : data.entrySet () ) + { + ctx.put ( e.getKey(), e.getValue() ); + } + fServlet.getVelocity ().mergeTemplate ( templateName, "UTF-8", ctx, to ); + } + + private final HttpServletRequest fRequest; + private final HttpServletResponse fResponse; + final DrumlinConnection fSession; + private final DrumlinServlet fServlet; + private final VelocityContext fBaseRenderContext; + final HashMap fLocalContext; + private final Map fObjects; + private final DrumlinRequestRouter fRouter; + + private final DrumlinRequest fRequestImpl; + + static org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinRequestContext.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinResponse.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinResponse.java new file mode 100644 index 0000000..4a9d514 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/DrumlinResponse.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Map; + +/** + * A response to a DrumlinRequest. + * + * @author peter@rathravane.com + * + */ +public interface DrumlinResponse +{ + /** + * send an error using the servlet container's error page system + * + * @param err + * @param msg + */ + void sendError ( int err, String msg ); + + /** + * set the status code for the reply + * + * @param code + */ + DrumlinResponse setStatus ( int code ); + int getStatusCode (); + + DrumlinResponse setContentType ( String mimeType ); + + DrumlinResponse send ( String content ) throws IOException; + + /** + * send an error response with the given body + * + * @param err + * @param content + * @param mimeType + * @throws IOException + */ + void sendErrorAndBody ( int err, String content, String mimeType ); + + PrintWriter getStreamForTextResponse () + throws IOException; + + PrintWriter getStreamForTextResponse ( String contentType ) + throws IOException; + + OutputStream getStreamForBinaryResponse () + throws IOException; + + OutputStream getStreamForBinaryResponse ( String contentType ) + throws IOException; + + void writeHeader ( String headerName, String headerValue ); + + void writeHeader ( String headerName, String headerValue, boolean overwrite ); + + /** + * redirect the to the app-relative url + * + * @param url + */ + void redirect ( String url ); + + void redirect ( Class cls, String method ); + + void redirect ( Class cls, String method, Map args ); + + /** + * redirect to the exact url + * + * @param url + */ + void redirectExactly ( String url ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRenderer.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRenderer.java new file mode 100644 index 0000000..91f9472 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRenderer.java @@ -0,0 +1,85 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +import org.apache.velocity.exception.ResourceNotFoundException; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.rendering.DrumlinRenderContext; +import com.att.nsa.drumlin.service.standards.HttpStatusCodes; + +class StdRenderer implements DrumlinRenderContext +{ + StdRenderer ( DrumlinRequestContext hc ) + { + fContext = hc; + } + + @Override + public Object get ( String key ) + { + return fContext.fLocalContext.get ( key ); + } + + @Override + public void put ( String key, Object o ) + { + fContext.fLocalContext.put ( key, o ); + } + + @Override + public void remove ( String key ) + { + fContext.fLocalContext.remove ( key ); + } + + @Override + public void renderTemplate ( String templateName ) + { + renderTemplate ( templateName, "text/html" ); + } + + @Override + public void renderTemplate ( String templateName, String contentType ) + { + try + { + if ( fContext.fSession != null ) + { + final HashMap context = new HashMap (); + fContext.fSession.buildTemplateContext ( context ); + for ( Map.Entry e : context.entrySet () ) + { + put ( e.getKey (), e.getValue () ); + } + } + + final PrintWriter out = fContext.response().getStreamForTextResponse ( contentType ); + fContext.merge ( templateName, fContext.fLocalContext, out ); + } + catch ( ResourceNotFoundException e ) + { + fContext.response ().sendError ( HttpStatusCodes.k404_notFound, e.getMessage () ); + } + catch ( Exception e ) + { + fContext.response ().sendError ( HttpStatusCodes.k500_internalServerError, e.getMessage () ); + + final StringWriter sw = new StringWriter (); + final PrintWriter pw = new PrintWriter ( sw ); + e.printStackTrace ( pw ); + pw.close (); + log.error ( sw.toString () ); + } + } + + private final DrumlinRequestContext fContext; + static org.slf4j.Logger log = LoggerFactory.getLogger ( StdRenderer.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRequest.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRequest.java new file mode 100644 index 0000000..b3f2ffe --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdRequest.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +class StdRequest implements DrumlinRequest +{ + public StdRequest ( HttpServletRequest r ) + { + fRequest = r; + fParamOverrides = new HashMap (); + } + + @Override + public String getUrl () + { + return fRequest.getRequestURL ().toString (); + } + + @Override + public boolean isSecure () + { + return fRequest.isSecure (); + } + + @Override + public String getQueryString () + { + final String qs = fRequest.getQueryString (); + if ( qs != null && qs.length () == 0 ) return null; + return qs; + } + + @Override + public String getMethod () + { + return fRequest.getMethod (); + } + + @Override + public String getPathInContext () + { + final String ctxPath = fRequest.getContextPath (); + final int ctxPathLen = ctxPath.length(); + return fRequest.getRequestURI().substring ( ctxPathLen ); + } + + @Override + public String getFirstHeader ( String h ) + { + List l = getHeader ( h ); + return ( l.size () > 0 ) ? l.iterator ().next () : null; + } + + @Override + public List getHeader ( String h ) + { + final LinkedList list = new LinkedList (); + final Enumeration e = fRequest.getHeaders ( h ); + while ( e.hasMoreElements () ) + { + list.add ( e.nextElement ().toString () ); + } + return list; + } + + @Override + public String getContentType () + { + return fRequest.getContentType (); + } + + @Override + public int getContentLength () + { + return fRequest.getContentLength (); + } + + @Override + public InputStream getBodyStream () + throws IOException + { + return fRequest.getInputStream (); + } + + @Override + public BufferedReader getBodyStreamAsText () + throws IOException + { + return new BufferedReader ( new InputStreamReader ( getBodyStream () ) ); + } + + @Override + public Map getParameterMap () + { + final HashMap map = new HashMap(); + final Map m = fRequest.getParameterMap (); + map.putAll ( m ); + map.putAll ( fParamOverrides ); + return map; + } + + @Override + public String getParameter ( String key ) + { + if ( fParamOverrides.containsKey ( key ) ) + { + final String[] o = fParamOverrides.get ( key ); + return o.length > 0 ? o[0] : ""; + } + else + { + return fRequest.getParameter ( key ); + } + } + + @Override + public String getParameter ( String key, String defVal ) + { + String p = getParameter ( key ); + if ( p == null ) + { + p = defVal; + } + return p; + } + + @Override + public int getIntParameter ( String key, int defVal ) + { + int result = defVal; + final String p = getParameter ( key ); + if ( p != null ) + { + try + { + result = Integer.parseInt ( p ); + } + catch ( Exception x ) + { + result = defVal; + } + } + return result; + } + + @Override + public void changeParameter ( String fieldName, String value ) + { + fParamOverrides.put ( fieldName, new String[] { value } ); + } + + @Override + public String getRemoteAddress () + { + final String reqAddr = fRequest.getRemoteAddr (); + final String fwdHeader = getFirstHeader ( "X-Forwarded-For" ); + return fwdHeader != null ? fwdHeader : reqAddr; + } + + private final HttpServletRequest fRequest; + private final HashMap fParamOverrides; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdResponse.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdResponse.java new file mode 100644 index 0000000..dbd36b7 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/context/stdResponse.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.context; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.service.standards.HttpMethods; +import com.att.nsa.drumlin.service.standards.MimeTypes; + +class StdResponse implements DrumlinResponse +{ + public StdResponse ( HttpServletRequest req, HttpServletResponse r, DrumlinRequestRouter rr ) + { + fRequest = req; + fResponseEntityAllowed = !(req.getMethod ().equalsIgnoreCase ( HttpMethods.HEAD )); + fResponse = r; + fRouter = rr; + writeHeader ( "X-Rathravane", "~ software is craft ~", true ); + } + + @Override + public void sendErrorAndBody ( int err, String content, String mimeType ) + { + try + { + setStatus ( err ); + getStreamForTextResponse ( mimeType ).println ( content ); + } + catch ( IOException e ) + { + log.warn ( "Error sending error response: " + e.getMessage () ); + } + } + + @Override + public void sendError ( int err, String msg ) + { + try + { + fResponse.sendError ( err, msg ); + } + catch ( IOException e ) + { + log.error ( "Error sending response: " + e.getMessage () ); + } + } + + @Override + public DrumlinResponse setStatus ( int code ) + { + fResponse.setStatus ( code ); + return this; + } + + @Override + public int getStatusCode () + { + return fResponse.getStatus (); + } + + @Override + public DrumlinResponse setContentType ( String mimeType ) + { + fResponse.setContentType ( mimeType ); + return this; + } + + @Override + public DrumlinResponse send ( String content ) throws IOException + { + final PrintWriter pw = new PrintWriter ( fResponse.getWriter () ); + pw.print ( content ); + pw.close (); + return this; + } + + @Override + public void writeHeader ( String headerName, String headerValue ) + { + writeHeader ( headerName, headerValue, false ); + } + + @Override + public void writeHeader ( String headerName, String headerValue, boolean overwrite ) + { + if ( overwrite ) + { + fResponse.setHeader ( headerName, headerValue ); + } + else + { + fResponse.addHeader ( headerName, headerValue ); + } + } + + @Override + public OutputStream getStreamForBinaryResponse () throws IOException + { + return getStreamForBinaryResponse ( MimeTypes.kAppGenericBinary ); + } + + @Override + public OutputStream getStreamForBinaryResponse ( String contentType ) throws IOException + { + fResponse.setContentType ( contentType ); + + OutputStream os = null; + if ( fResponseEntityAllowed ) + { + os = fResponse.getOutputStream (); + } + else + { + os = new NullStream (); + } + return os; + } + + @Override + public PrintWriter getStreamForTextResponse () + throws IOException + { + return getStreamForTextResponse ( "text/html" ); + } + + @Override + public PrintWriter getStreamForTextResponse ( String contentType ) throws IOException + { + fResponse.setContentType ( contentType ); + + PrintWriter pw = null; + if ( fResponseEntityAllowed ) + { + pw = fResponse.getWriter (); + } + else + { + pw = new PrintWriter ( new NullWriter () ); + } + return pw; + } + + @Override + public void redirect ( String url ) + { + redirectExactly ( DrumlinRequestContext.servletPathToFullPath ( url, fRequest ) ); + } + + @Override + public void redirect ( Class cls, String method ) + { + redirect ( cls, method, new HashMap () ); + } + + @Override + public void redirect ( Class cls, String method, Map args ) + { + String localUrl = fRouter.reverseRoute ( cls, method, args ); + if ( localUrl == null ) + { + log.error ( "No reverse route for " + cls.getName () + "::" + method + " with " + (args == null ? 0 : args.size () ) + " args." ); + localUrl = "/"; + } + redirect ( localUrl ); + } + + @Override + public void redirectExactly ( String url ) + { + try + { + fResponse.sendRedirect ( url ); + } + catch ( IOException e ) + { + log.error ( "Error sending redirect: " + e.getMessage () ); + } + } + + private final HttpServletRequest fRequest; + private final boolean fResponseEntityAllowed; + private final HttpServletResponse fResponse; + private final DrumlinRequestRouter fRouter; + + private static org.slf4j.Logger log = LoggerFactory.getLogger ( StdResponse.class ); + + private static class NullWriter extends Writer + { + @Override + public void write ( char[] cbuf, int off, int len ) + { + } + + @Override + public void flush () + { + } + + @Override + public void close () + { + } + } + + private static class NullStream extends OutputStream + { + @Override + public void write ( int b ) {} + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/DrumlinRenderContext.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/DrumlinRenderContext.java new file mode 100644 index 0000000..f3850ec --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/DrumlinRenderContext.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.rendering; + +/** + * An interface to the Velocity renderer. Get/put/remove objects + * for use in VTL; Render templates to the response stream (provided + * by the handlingContext where the renderContext was acquired). + * + * @author peter + */ +public interface DrumlinRenderContext +{ + /** + * Get an object in the context by name. + * @param key + * @return an object, or null. + */ + Object get ( String key ); + + /** + * Put an object into the render context with a name. The name is available + * in Velocity VTL. + * + * @param key + * @param o + */ + void put ( String key, Object o ); + + /** + * Remove an object given its name. + * @param key + */ + void remove ( String key ); + + /** + * Render the named template. + * @param templateName + */ + void renderTemplate ( String templateName ); + + /** + * Render the named template with the given content type in the HTTP header. + * @param templateName + * @param contentType + */ + void renderTemplate ( String templateName, String contentType ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/vtlTools/DrumlinVtlHelper.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/vtlTools/DrumlinVtlHelper.java new file mode 100644 index 0000000..3552a92 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/rendering/vtlTools/DrumlinVtlHelper.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.rendering.vtlTools; + +import com.att.nsa.drumlin.till.data.rrConvertor; + +public class DrumlinVtlHelper +{ + public String noBreakingSpace ( String e ) + { + return replace ( e, " ", " " ); + } + + public String replace ( String e, String from, String to ) + { + return e.replace ( from, to ); + } + + public String encode ( String e ) + { + return rrConvertor.urlEncode ( e ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRequestRouter.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRequestRouter.java new file mode 100644 index 0000000..a95fc5d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRequestRouter.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.DrumlinErrorHandler; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A Drumlin request router is configured with route sources and error handlers, then + * used to route an incoming request to a request handler. + * + * @author peter@rathravane.com + * + */ +public class DrumlinRequestRouter +{ + /** + * no matching route exception + */ + public static class noMatchingRoute extends Exception + { + public noMatchingRoute ( String route ) { super ( "No route for '" + route + "'" ); } + private static final long serialVersionUID = 1L; + } + + /** + * construct a request router + */ + public DrumlinRequestRouter () + { + fSources = new LinkedList (); + fErrorHandlers = new HashMap,DrumlinErrorHandler> (); + } + + /** + * add a route source + * @param src + */ + public synchronized void addRouteSource ( DrumlinRouteSource src ) + { + fSources.add ( src ); + } + + /** + * Provide a URL for redirects when there's no specific error handler for the exception. + * @param url + */ + public synchronized void setGeneralErrorRedirectUrl ( final String url ) + { + fErrorHandlers.put ( Throwable.class, new DrumlinErrorHandler () + { + @Override + public void handle ( DrumlinRequestContext ctx, Throwable cause ) + { + log.info ( "General error handler invoked, redirect to " + url ); + ctx.response ().redirect ( url ); + } + } ); + } + + /** + * Set an error handler for a specific class of throwable. + * @param x + * @param eh + */ + public synchronized void setHandlerForException ( Class x, DrumlinErrorHandler eh ) + { + fErrorHandlers.put ( x, eh ); + } + + /** + * Given an incoming request, check each route source (in order) for a match. If the route source + * has a match, it's used to handle the request. + * + * @param req + * @return a matching handler + * @throws noMatchingRoute + */ + public synchronized DrumlinRouteInvocation route ( DrumlinRequest req ) throws noMatchingRoute + { + final String verbIn = req.getMethod (); + final String verb = verbIn.equalsIgnoreCase("HEAD")?"GET":verbIn; // HEAD is GET without an entity response + + final String path = req.getPathInContext (); + + DrumlinRouteInvocation route = null; + for ( DrumlinRouteSource src : fSources ) + { + route = src.getRouteFor ( verb, path ); + if ( route != null ) + { + break; + } + } + + if ( route == null ) + { + log.warn ( "No match for " + verb + " " + path ); + throw new noMatchingRoute ( path ); + } + + return route; + } + + /** + * Find the proper handler for a throwable. + * @param cause + * @return an error handler, or null if none are applicable + */ + public synchronized DrumlinErrorHandler route ( Throwable cause ) + { + DrumlinErrorHandler h = null; + Class c = cause.getClass (); + while ( h == null && c != null ) + { + h = fErrorHandlers.get ( c ); + if ( h == null ) + { + c = c.getSuperclass (); + } + } + return h; + } + + /** + * Given a handler class and the name of one of its static methods, return a + * registered route to it. + * + * @param c + * @param staticMethodName + * @return + */ + public synchronized String reverseRoute ( Class c, String staticMethodName ) + { + return reverseRoute ( c, staticMethodName, new HashMap () ); + } + + /** + * Given a handler class, the name of one of its static methods, and some arguments, + * return a registered route to the handler method. + * + * @param c + * @param staticMethodName + * @param args + * @return + */ + public synchronized String reverseRoute ( Class c, String staticMethodName, Map args ) + { + String route = null; + for ( DrumlinRouteSource src : fSources ) + { + route = src.getRouteTo ( c, staticMethodName, args ); + if ( route != null ) + { + break; + } + } + return route; + } + + private final LinkedList fSources; + private final HashMap,DrumlinErrorHandler> fErrorHandlers; + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinRequestRouter.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteInvocation.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteInvocation.java new file mode 100644 index 0000000..b75ec6b --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteInvocation.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A route invocation is returned by a route source as a match for an incoming route. It's then run() + * to execute the request handling. + * + * @author peter@rathravane.com + * + */ +public interface DrumlinRouteInvocation +{ + /** + * Get the route's name + * @return the route name + */ + String getName (); + + /** + * Run the request + * @param ctx + * @throws IOException + * @throws IllegalArgumentException + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + void run ( DrumlinRequestContext ctx ) throws IOException, IllegalArgumentException, IllegalAccessException, InvocationTargetException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteSource.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteSource.java new file mode 100644 index 0000000..022adf0 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinRouteSource.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing; + +import java.util.Map; + +/** + * A route source is a collection of routes that are requested by verb (e.g. GET) and + * a path. A Drumlin app can have any number of route sources. During request handling, + * each route source is tested in order via getRouteFor(). If the route source returns + * a {@link DrumlinRouteInvocation}, it's used to handle the request. + * + * @author peter@rathravane.com + * + */ +public interface DrumlinRouteSource +{ + /** + * Return the route handler for a given verb and path or null. + * @param verb + * @param path + * @return + */ + DrumlinRouteInvocation getRouteFor ( String verb, String path ); + + /** + * Code in this system can create a URL to get to a specific class + method by asking + * the router to find a reverse-route. If this route source has routes that point to + * static entry points, it should implement an override that returns the correct URL. + * + * @param c + * @param staticMethodName + * @param args + * @return null, or a URL to get to the entry point + */ + String getRouteTo ( Class c, String staticMethodName, Map args ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinSimpleRouteHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinSimpleRouteHandler.java new file mode 100644 index 0000000..f711a75 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/DrumlinSimpleRouteHandler.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing; + +import java.io.IOException; +import java.util.Map; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A basic route handler provided for convenience in creating simple handlers. + * + * @author peter@rathravane.com + */ +public abstract class DrumlinSimpleRouteHandler implements DrumlinRouteSource, DrumlinRouteInvocation +{ + public DrumlinRouteInvocation getRouteFor ( String verb, String path ) + { + return this; + } + + public String getRouteTo ( Class c, String staticMethodName, Map args ) + { + return null; + } + + public abstract void run ( DrumlinRequestContext ctx ) throws IOException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPathInfo.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPathInfo.java new file mode 100644 index 0000000..3827145 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPathInfo.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author peter + * + */ +public class DrumlinPathInfo +{ + public DrumlinPathInfo ( String verb, String path, List args, Pattern pattern ) + { + fVerb = verb; + fPath = path; + fHandler = null; + fArgs = args; + fPathPattern = pattern; + } + + @Override + public String toString () + { + return fVerb + " " + fPath + " ==> " + ( fHandler != null ? fHandler.toString () : "(null)" ); + } + + public void setHandler ( DrumlinPlayishRouteHandler handler ) + { + fHandler = handler; + } + + public DrumlinPlayishRouteHandler getHandler () + { + return fHandler; + } + + /** + * make a path to this handler given a set of named arguments + * @param args + * @return a path that will invoke this handler + */ + public String makePath ( Map args ) + { + final StringBuffer result = new StringBuffer (); + + if ( fPath.contains ( "{" ) ) + { + String remains = fPath; + while ( remains.length () > 0 ) + { + final int brace = remains.indexOf ( '{' ); + if ( brace > -1 ) + { + final int close = remains.indexOf ( '}', brace ); + if ( close == -1 ) + { + throw new IllegalArgumentException ( "Opened brace but didn't close it." ); + } + + result.append ( remains.substring ( 0, brace ) ); + + String name = remains.substring ( brace + 1, close ); + if ( name.startsWith ( "<" ) ) + { + int closeBracket = name.indexOf ( '>' ); + if ( closeBracket > -1 ) + { + name = name.substring ( closeBracket+1 ); + } + } + + final Object o = args == null ? null : args.get ( name ); + if ( o != null ) + { + result.append ( args.get ( name ) ); + } + else + { + result.append ( "{" ); + result.append ( name ); + result.append ( "}" ); + } + + remains = remains.substring ( close + 1 ); + } + else + { + result.append ( remains ); + remains = ""; + } + } + } + else + { + result.append ( fPath ); + } + return result.toString (); + } + + public List getArgs () + { + return new LinkedList ( fArgs ); + } + + private final String fVerb; + private final String fPath; + private DrumlinPlayishRouteHandler fHandler; + private final Pattern fPathPattern; + private final List fArgs; + + public List matches ( String verb, String path ) + { + LinkedList result = null; + if ( verb != null && path != null && verb.equalsIgnoreCase ( fVerb ) ) + { + final Matcher m = fPathPattern.matcher ( path ); + final int argCount = fArgs.size (); + if ( m.matches () )// && m.groupCount () == argCount ) + { + result = new LinkedList (); + for ( int i=1; i<=argCount; i++ ) + { + final String part = m.group ( i ); + final String decode = decode ( part ); + result.add ( decode ); + } + } + } + return result; + } + + public static DrumlinPathInfo processPath ( String verb, String path ) + { + final LinkedList args = new LinkedList (); + + String fullPathRegex = path; + if ( path.contains ( "{" ) ) + { + // the path needs processing + + fullPathRegex = ""; + String remains = path; + while ( remains.length () > 0 ) + { + final int brace = remains.indexOf ( '{' ); + if ( brace > -1 ) + { + final String preVar = remains.substring ( 0, brace ); + final int close = remains.indexOf ( '}', brace ); + if ( close == -1 ) + { + throw new IllegalArgumentException ( "Opened brace but didn't close it." ); + } + + final String inner = remains.substring ( brace + 1, close ); + String regex = "([^/]+)"; + String argName = inner; + if ( inner.startsWith ( "<" ) ) + { + final int bracketClose = inner.indexOf ( '>' ); + if ( bracketClose == -1 ) + { + throw new IllegalArgumentException ( "Opened bracket but didn't close it." ); + } + + argName = inner.substring ( bracketClose + 1 ); + regex = "(" + inner.substring ( 1, bracketClose ) + ")"; + } + + args.add ( argName ); + + fullPathRegex += preVar; + fullPathRegex += regex; + remains = remains.substring ( close + 1 ); + } + else + { + fullPathRegex += remains; + remains = ""; + } + } + } + final Pattern pathPattern = Pattern.compile ( fullPathRegex ); + return new DrumlinPathInfo ( verb, path, args, pathPattern ); + } + + public boolean invokes ( String fullName ) + { + return fHandler.actionMatches ( fullName ); + } + + private static String decode ( String s ) + { + try + { + return URLDecoder.decode ( s, "UTF-8" ); + } + catch ( UnsupportedEncodingException e ) + { + throw new RuntimeException ( e ); + } + } + + public String getVerb () + { + return fVerb; + } + + public String getPath () + { + return fPath; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishInstanceCallRoutingSource.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishInstanceCallRoutingSource.java new file mode 100644 index 0000000..d99d926 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishInstanceCallRoutingSource.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteInvocation; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteSource; + +/** + * This routing source routes to methods on an object instance. + * + * @author peter@rathravane.com + * + */ +public class DrumlinPlayishInstanceCallRoutingSource implements DrumlinRouteSource +{ + public DrumlinPlayishInstanceCallRoutingSource ( Object o ) + { + fInstance = o; + fPathList = new LinkedList (); + fPackages = new LinkedList (); + } + + /** + * Add a verb and path route with an action string. The action can start with "staticDir:" or + * "staticFile:". The remainder of the string is used as a relative filename to the dir (staticDir:), or + * as a filename (staticFile:). + * @param verb + * @param path + * @param action + * @return this object (for use in chaining the add calls) + */ + public synchronized DrumlinPlayishInstanceCallRoutingSource addRoute ( String verb, String path, String action ) + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path ); + pe.setHandler ( new InstanceEntryAction ( fInstance, action, pe.getArgs(), fPackages ) ); + fPathList.add ( pe ); + + return this; + } + + /** + * Get a route invocation for a given verb+path, or null. + */ + @Override + public synchronized DrumlinRouteInvocation getRouteFor ( String verb, String path ) + { + DrumlinRouteInvocation selected = null; + for ( DrumlinPathInfo pe : fPathList ) + { + final List args = pe.matches ( verb, path ); + if ( args != null ) + { + selected = getInvocation ( pe, args ); + break; + } + } + return selected; + } + + /** + * Get the URL that reaches a given static method with the given arguments. + */ + @Override + public String getRouteTo ( Class c, String staticMethodName, Map args ) + { + final String fullname = c.getName() + "." + staticMethodName; + for ( DrumlinPathInfo pe : fPathList ) + { + if ( pe.invokes ( fullname ) ) + { + return pe.makePath ( args ); + } + } + return null; + } + + private final Object fInstance; + private final LinkedList fPackages; + private final LinkedList fPathList; + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinPlayishInstanceCallRoutingSource.class ); + + protected invocation getInvocation ( DrumlinPathInfo pe, List args ) + { + return new invocation ( pe, args ); + } + + protected class invocation implements DrumlinRouteInvocation + { + public invocation ( DrumlinPathInfo pe, List args ) + { + fPe = pe; + fArgs = args; + } + + @Override + public String getName () + { + return fPe.getVerb () + "_" + fPe.getPath (); + } + + @Override + public void run ( DrumlinRequestContext ctx ) throws IOException, IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + fPe.getHandler ().handle ( ctx, fArgs ); + } + + private final DrumlinPathInfo fPe; + private final List fArgs; + } + + protected synchronized void clearRoutes () + { + log.debug ( "Clearing routes within this instance route source." ); + fPathList.clear (); + } + + protected synchronized void addPackage ( String pkg ) + { + fPackages.add ( pkg ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRouteHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRouteHandler.java new file mode 100644 index 0000000..9142867 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRouteHandler.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A route handler handles a request, given a context. + * @author peter + */ +public interface DrumlinPlayishRouteHandler +{ + void handle ( DrumlinRequestContext context, List args ) throws IOException, IllegalArgumentException, IllegalAccessException, InvocationTargetException; + boolean actionMatches ( String fullPath ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRoutingFileSource.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRoutingFileSource.java new file mode 100644 index 0000000..440f753 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishRoutingFileSource.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +import org.slf4j.LoggerFactory; + +public class DrumlinPlayishRoutingFileSource extends DrumlinPlayishStaticEntryPointRoutingSource +{ + public DrumlinPlayishRoutingFileSource ( File f ) throws IOException + { + this ( f, true ); + } + + public DrumlinPlayishRoutingFileSource ( File f, boolean withAutoRefresh ) throws IOException + { + super (); + + loadRoutes ( f ); + createRefreshThread ( f, withAutoRefresh ); + } + + public DrumlinPlayishRoutingFileSource ( URL u ) throws IOException + { + super (); + + if ( u == null ) + { + throw new IOException ( "URL for routing file is null in drumlinPlayStyleRoutingFileSource ( URL u )" ); + } + loadRoutes ( u ); + } + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinPlayishRoutingFileSource.class ); + + private synchronized void loadRoutes ( URL u ) throws IOException + { + loadRoutes ( new InputStreamReader ( u.openStream () ) ); + } + + private synchronized void loadRoutes ( File f ) throws IOException + { + loadRoutes ( new FileReader ( f ) ); + } + + private synchronized void loadRoutes ( Reader r ) throws IOException + { + clearRoutes (); + + final BufferedReader fr = new BufferedReader ( r ); + + String line; + while ( ( line = fr.readLine () ) != null ) + { + line = line.trim (); + if ( line.length () > 0 && !line.startsWith ( "#" ) ) + { + processLine ( line ); + } + } + } + + private void processLine ( String line ) + { + try + { + final StringTokenizer st = new StringTokenizer ( line ); + final String verb = st.nextToken (); + if ( verb.toLowerCase ().equals ( "package" ) ) + { + final String pkg = st.nextToken (); + addPackage ( pkg ); + } + else + { + final String path = st.nextToken (); + final String action = st.nextToken (); + addRoute ( verb, path, action ); + } + } + catch ( NoSuchElementException e ) + { + log.warn ( "There was an error processing route config line: \"" + line + "\"" ); + } + catch ( IllegalArgumentException e ) + { + log.warn ( "There was an error processing route config line: \"" + line + "\": " + e.getMessage () ); + } + } + + private Thread createRefreshThread ( final File f, boolean routeRefresh ) + { + Thread result = null; + if ( routeRefresh ) + { + result = new Thread () + { + private long fLastMod = f.lastModified (); + + @Override + public void run () + { + try + { + sleep ( 2000 ); + } + catch ( InterruptedException e1 ) + { + // ignore + } + + final long lastMod = f.lastModified (); + if ( lastMod > fLastMod ) + { + log.info ( "Reloading routes from " + f.getAbsolutePath () ); + try + { + fLastMod = lastMod; + loadRoutes ( f ); + } + catch ( IOException e ) + { + log.error ( "Unable to load route file", e ); + } + } + } + }; + result.setName ( "Route file update watcher for " + f.getName () + "." ); + result.setDaemon ( true ); + result.start (); // FIXME: clunky, and not cool to start thread in constructor's scope + } + return result; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishStaticEntryPointRoutingSource.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishStaticEntryPointRoutingSource.java new file mode 100644 index 0000000..55e20a8 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/DrumlinPlayishStaticEntryPointRoutingSource.java @@ -0,0 +1,164 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteInvocation; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteSource; + +/** + * A static entry point routing source is a collection of routing entries for mapping request + * paths to static files and directories. + * + * @author peter@rathravane.com + * + */ +public class DrumlinPlayishStaticEntryPointRoutingSource implements DrumlinRouteSource +{ + public DrumlinPlayishStaticEntryPointRoutingSource () + { + fPathList = new LinkedList (); + fPackages = new LinkedList (); + } + + /** + * Add a verb and path route with an action string. The action can start with "staticDir:" or + * "staticFile:". The remainder of the string is used as a relative filename to the dir (staticDir:), or + * as a filename (staticFile:). + * @param verb + * @param path + * @param action + * @return this object (for use in chaining the add calls) + */ + public synchronized DrumlinPlayishStaticEntryPointRoutingSource addRoute ( String verb, String path, String action ) + { + if ( action.startsWith ( kStaticDirTag ) ) + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path + ".*" ); + pe.setHandler ( new StaticDirHandler ( path, action.substring ( kStaticDirTag.length () ) ) ); + fPathList.add ( pe ); + } + else if ( action.startsWith ( kStaticFileTag ) ) + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path ); + pe.setHandler ( new StaticFileHandler ( path, action.substring ( kStaticFileTag.length () ) ) ); + fPathList.add ( pe ); + } + else if ( action.startsWith ( kRedirectTag ) ) + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path ); + final String loc = action.substring ( kRedirectTag.length () ); + pe.setHandler ( new RedirectHandler ( loc ) ); + fPathList.add ( pe ); + } + else if ( action.startsWith ( kTemplateDirTag ) ) + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path + ".*" ); + pe.setHandler ( new TemplateDirHandler ( "/" + action.substring ( kTemplateDirTag.length () ) ) ); + fPathList.add ( pe ); + } + else + { + final DrumlinPathInfo pe = DrumlinPathInfo.processPath ( verb, path ); + pe.setHandler ( new StaticJavaEntryAction ( action, pe.getArgs(), fPackages ) ); + fPathList.add ( pe ); + } + return this; + } + + /** + * Get a route invocation for a given verb+path, or null. + */ + @Override + public synchronized DrumlinRouteInvocation getRouteFor ( String verb, String path ) + { + DrumlinRouteInvocation selected = null; + for ( DrumlinPathInfo pe : fPathList ) + { + final List args = pe.matches ( verb, path ); + if ( args != null ) + { + selected = getInvocation ( pe, args ); + break; + } + } + return selected; + } + + /** + * Get the URL that reaches a given static method with the given arguments. + */ + @Override + public String getRouteTo ( Class c, String staticMethodName, Map args ) + { + final String fullname = c.getName() + "." + staticMethodName; + for ( DrumlinPathInfo pe : fPathList ) + { + if ( pe.invokes ( fullname ) ) + { + return pe.makePath ( args ); + } + } + return null; + } + + private final LinkedList fPackages; + private final LinkedList fPathList; + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinPlayishStaticEntryPointRoutingSource.class ); + + protected invocation getInvocation ( DrumlinPathInfo pe, List args ) + { + return new invocation ( pe, args ); + } + + protected class invocation implements DrumlinRouteInvocation + { + public invocation ( DrumlinPathInfo pe, List args ) + { + fPe = pe; + fArgs = args; + } + + @Override + public String getName () + { + return fPe.getVerb () + "_" + fPe.getPath (); + } + + @Override + public void run ( DrumlinRequestContext ctx ) throws IOException, IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + fPe.getHandler ().handle ( ctx, fArgs ); + } + + private final DrumlinPathInfo fPe; + private final List fArgs; + } + + protected synchronized void clearRoutes () + { + log.debug ( "Clearing routes within this static route source." ); + fPathList.clear (); + } + + protected synchronized void addPackage ( String pkg ) + { + fPackages.add ( pkg ); + } + + private static final String kStaticDirTag = "staticDir:"; + private static final String kStaticFileTag = "staticFile:"; + private static final String kRedirectTag = "redirect:"; + private static final String kTemplateDirTag = "templateDir:"; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/InstanceEntryAction.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/InstanceEntryAction.java new file mode 100644 index 0000000..a59e7e6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/InstanceEntryAction.java @@ -0,0 +1,137 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.List; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class InstanceEntryAction implements DrumlinPlayishRouteHandler +{ + public InstanceEntryAction ( Object instance, String action, List args, Collection packages ) + { + fInstance = instance; + fAction = action; + fArgs = args; + fMethod = null; + + processAction ( packages ); + } + + @Override + public String toString () + { + return fAction; + } + + @Override + public void handle ( DrumlinRequestContext context, List addlArgs ) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + final Object[] methodArgs = new Object[addlArgs.size ()+1]; + methodArgs[0] = context; + int i=1; + for ( String arg : addlArgs ) + { + methodArgs[i++] = arg; + } + fMethod.invoke ( fInstance, methodArgs ); + } + + @Override + public boolean actionMatches ( String fullPath ) + { + return fAction.equals ( fullPath ); + } + + private final Object fInstance; + private String fAction; + private final List fArgs; + private Method fMethod; + + private void processAction ( Collection packages ) + { + final int lastDot = fAction.lastIndexOf ( "." ); + if ( lastDot < 0 ) + { + throw new IllegalArgumentException ( "The action string should have at least \"class.method\"." ); + } + + final String className = fAction.substring ( 0, lastDot ); + final String methodName = fAction.substring ( lastDot + 1 ); + + try + { + final Class c = locateClass ( className, packages ); + fAction = c.getName () + "." + methodName; + + final Class[] s = new Class[ fArgs.size () + 1 ]; + s[0] = DrumlinRequestContext.class; + for ( int i=1; i<=fArgs.size(); i++ ) + { + s[i] = String.class; + } + fMethod = c.getMethod ( methodName, s ); + if ( Modifier.isStatic ( fMethod.getModifiers () ) ) + { + throw new IllegalArgumentException ( methodName + " is static." ); + } + } + catch ( ClassNotFoundException e ) + { + throw new IllegalArgumentException ( e ); + } + catch ( SecurityException e ) + { + throw new IllegalArgumentException ( e ); + } + catch ( NoSuchMethodException e ) + { + throw new IllegalArgumentException ( e ); + } + } + + private Class locateClass ( String name, Collection packages ) throws ClassNotFoundException + { + // try it straight... + Class result = tryClass ( name ); + if ( result == null ) + { + // try the package list + for ( String pkg : packages ) + { + result = tryClass ( pkg + "." + name ); + if ( result != null ) break; + } + } + if ( result == null ) + { + throw new ClassNotFoundException ( name ); + } + return result; + } + + private Class tryClass ( String name ) + { + Class result = null; + try + { + result = Class.forName ( name ); + log.debug ( "class [" + name + "] located" ); + } + catch ( ClassNotFoundException e ) + { + log.debug ( "class [" + name + "] not found" ); + } + return result; + } + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( InstanceEntryAction.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/RedirectHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/RedirectHandler.java new file mode 100644 index 0000000..d6e9eeb --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/RedirectHandler.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.util.List; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class RedirectHandler implements DrumlinPlayishRouteHandler +{ + public static final String kMaxAge = "drumlin.staticDir.cache.maxAgeSeconds"; + + public RedirectHandler ( String loc ) + { + fTargetLocation = loc; + } + + @Override + public void handle ( DrumlinRequestContext context, List args ) + { + context.response ().redirect ( fTargetLocation ); + } + + private final String fTargetLocation; + + @Override + public boolean actionMatches(String fullPath) + { + return false; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticDirHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticDirHandler.java new file mode 100644 index 0000000..7e8c4bb --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticDirHandler.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.HashMap; +import java.util.List; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.standards.MimeTypes; +import com.att.nsa.util.StreamTools; + +public class StaticDirHandler implements DrumlinPlayishRouteHandler +{ + public static final String kMaxAge = StaticFileHandler.kMaxAge; + + public StaticDirHandler ( String routedPath, String staticDirInfo ) + { + // the format of staticDirInfo is "dir;defaultpage" + final String[] parts = staticDirInfo.split ( ";" ); + if ( parts.length < 1 ) throw new IllegalArgumentException ( "dir[;defaultpage]" ); + + fRoutedPath = routedPath; // e.g. "/css/" + fDir = parts[0]; // e.g. "css" + if ( parts.length > 1 ) + { + fDefaultPage = parts[1]; + } + else + { + fDefaultPage = null; + } + } + + @Override + public void handle ( DrumlinRequestContext context, List args ) + { + final String path = context.request ().getPathInContext (); + if ( path == null || path.length () == 0 ) + { + log.warn ( "404 [" + path + "] no path provided" ); + context.response ().sendError ( 404, "no path provided" ); + return; + } + + if ( path.contains ( ".." ) ) + { + log.warn ( "404 [" + path + "] contains parent directory accessor" ); + context.response ().sendError ( 404, path + " was not found on this server." ); + return; + } + + // here, the path should start with the "routed path" and we want to replace + // that with the local dir + if ( !path.startsWith ( fRoutedPath ) ) + { + log.warn ( "404 [" + path + "] does not start with routed path [" + fRoutedPath + "]" ); + context.response ().sendError ( 404, path + " is not a matching path" ); + return; + } + + // use "/" rather than File.separator because when running on windows, we wind + // up with the wrong path for classpath searches + final String newPath = fDir + "/" + path.substring ( fRoutedPath.length () ); + log.info ( "finding stream " + newPath ); + URL in = context.getServlet ().findStream ( newPath ); + if ( in == null && fDefaultPage != null ) + { + String defIn = newPath + "/" + fDefaultPage; + log.info ( "[" + newPath + "] does not exist, trying [" + defIn + "]." ); + URL defInFile = context.getServlet ().findStream ( defIn ); + if ( defInFile != null ) + { + log.info ( "[" + defInFile + "] found, using it." ); + in = defInFile; + } + } + + log.info ( "Path [" + path + "] ==> [" + ( in == null ? "" : in.toString () ) + "]." ); + if ( in == null ) + { + context.response ().sendError ( 404, path + " was not found on this server." ); + } + else + { + final String contentType = mapToContentType ( in.toString () ); + + // expiry. currently global. + final int cacheMaxAge = context.systemSettings ().getInt ( kMaxAge, -1 ); + if ( cacheMaxAge > 0 ) + { + context.response ().writeHeader ( "Cache-Control", "max-age=" + cacheMaxAge, true ); + } + + try + { + final InputStream is = in.openStream (); + final OutputStream os = context.response ().getStreamForBinaryResponse ( contentType ); + StreamTools.copyStream ( is, os ); + } + catch ( FileNotFoundException e ) + { + log.warn ( "404 [" + path + "]==>[" + path + "] (" + in.toString () + ")" ); + context.response ().sendError ( 404, path + " was not found on this server." ); + } + catch ( IOException e ) + { + log.warn ( "500 [" + in.toString () + "]: " + e.getMessage () ); + context.response ().sendError ( 500, e.getMessage () ); + } + } + } + + private final String fRoutedPath; + private final String fDir; + private final String fDefaultPage; + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( StaticDirHandler.class ); + + static final HashMap sfContentTypes = new HashMap (); + static + { + sfContentTypes.put ( "css", MimeTypes.kCss ); + + sfContentTypes.put ( "jpg", MimeTypes.kImageJpg ); + sfContentTypes.put ( "gif", MimeTypes.kImageGif ); + sfContentTypes.put ( "png", MimeTypes.kImagePng ); + sfContentTypes.put ( "ico", MimeTypes.kImageIco ); + + sfContentTypes.put ( "htm", MimeTypes.kHtml ); + sfContentTypes.put ( "html", MimeTypes.kHtml ); + + sfContentTypes.put ( "xml", MimeTypes.kAppXml ); + + sfContentTypes.put ( "js", MimeTypes.kAppJavascript ); + + sfContentTypes.put ( "eot", MimeTypes.kFontEot ); + sfContentTypes.put ( "woff", MimeTypes.kFontWoff ); + sfContentTypes.put ( "otf", MimeTypes.kFontOtf ); + sfContentTypes.put ( "ttf", MimeTypes.kFontTtf ); + sfContentTypes.put ( "svg", MimeTypes.kSvg ); + } + + public static String mapToContentType ( String name ) + { + final int dot = name.lastIndexOf ( "." ); + if ( dot != -1 ) + { + name = name.substring ( dot + 1 ); + } + String result = sfContentTypes.get ( name ); + if ( result == null ) + { + log.warn ( "Unknown content type [" + name + "]. Sending text/plain. (See " + StaticDirHandler.class.getSimpleName () + "::sfContentTypes)" ); + result = "text/plain"; + } + return result; + } + + @Override + public boolean actionMatches(String fullPath) + { + return false; + } +} + diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticFileHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticFileHandler.java new file mode 100644 index 0000000..eca4dcd --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticFileHandler.java @@ -0,0 +1,64 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.util.StreamTools; + +public class StaticFileHandler implements DrumlinPlayishRouteHandler +{ + public static final String kMaxAge = "drumlin.staticFile.cache.maxAgeSeconds"; + + public StaticFileHandler ( String routedPath, String staticFile ) + { + String file = staticFile.endsWith ( "/" ) ? ( staticFile + routedPath ) : staticFile; + file = file.replaceAll ( "//", "/" ); + + fFile = file; + fContentType = StaticDirHandler.mapToContentType ( fFile ); + } + + @Override + public void handle ( DrumlinRequestContext context, List args ) throws IOException + { + // expiry. currently global. + final int cacheMaxAge = context.systemSettings ().getInt ( kMaxAge, -1 ); + if ( cacheMaxAge > 0 ) + { + context.response().writeHeader ( "Cache-Control", "max-age=" + cacheMaxAge, true ); + } + + log.info ( "finding stream [" + fFile + "]" ); + final URL f = context.getServlet ().findStream ( fFile ); + if ( f == null ) + { + log.warn ( "404 [" + fFile + "] not found" ); + context.response ().sendError ( 404, fFile + " was not found on this server." ); + } + else + { + StreamTools.copyStream ( + f.openStream (), + context.response ().getStreamForBinaryResponse ( fContentType ) + ); + } + } + + @Override + public boolean actionMatches(String fullPath) + { + return false; + } + + private final String fFile; + private final String fContentType; + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( StaticFileHandler.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticJavaEntryAction.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticJavaEntryAction.java new file mode 100644 index 0000000..810a5b5 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/StaticJavaEntryAction.java @@ -0,0 +1,134 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.List; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class StaticJavaEntryAction implements DrumlinPlayishRouteHandler +{ + public StaticJavaEntryAction ( String action, List args, Collection packages ) + { + fAction = action; + fArgs = args; + fMethod = null; + + processAction ( packages ); + } + + @Override + public String toString () + { + return fAction; + } + + @Override + public void handle ( DrumlinRequestContext context, List addlArgs ) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException + { + final Object[] methodArgs = new Object[addlArgs.size ()+1]; + methodArgs[0] = context; + int i=1; + for ( String arg : addlArgs ) + { + methodArgs[i++] = arg; + } + fMethod.invoke ( null, methodArgs ); + } + + @Override + public boolean actionMatches ( String fullPath ) + { + return fAction.equals ( fullPath ); + } + + private String fAction; + private final List fArgs; + private Method fMethod; + + private void processAction ( Collection packages ) + { + final int lastDot = fAction.lastIndexOf ( "." ); + if ( lastDot < 0 ) + { + throw new IllegalArgumentException ( "The action string should have at least \"class.method\"." ); + } + + final String className = fAction.substring ( 0, lastDot ); + final String methodName = fAction.substring ( lastDot + 1 ); + + try + { + final Class c = locateClass ( className, packages ); + fAction = c.getName () + "." + methodName; + + final Class[] s = new Class[ fArgs.size () + 1 ]; + s[0] = DrumlinRequestContext.class; + for ( int i=1; i<=fArgs.size(); i++ ) + { + s[i] = String.class; + } + fMethod = c.getMethod ( methodName, s ); + if ( !Modifier.isStatic ( fMethod.getModifiers () ) ) + { + throw new IllegalArgumentException ( methodName + " is not static." ); + } + } + catch ( ClassNotFoundException e ) + { + throw new IllegalArgumentException ( e ); + } + catch ( SecurityException e ) + { + throw new IllegalArgumentException ( e ); + } + catch ( NoSuchMethodException e ) + { + throw new IllegalArgumentException ( e ); + } + } + + private Class locateClass ( String name, Collection packages ) throws ClassNotFoundException + { + // try it straight... + Class result = tryClass ( name ); + if ( result == null ) + { + // try the package list + for ( String pkg : packages ) + { + result = tryClass ( pkg + "." + name ); + if ( result != null ) break; + } + } + if ( result == null ) + { + throw new ClassNotFoundException ( name ); + } + return result; + } + + private Class tryClass ( String name ) + { + Class result = null; + try + { + result = Class.forName ( name ); + log.debug ( "class [" + name + "] located" ); + } + catch ( ClassNotFoundException e ) + { + log.debug ( "class [" + name + "] not found" ); + } + return result; + } + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( StaticJavaEntryAction.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/TemplateDirHandler.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/TemplateDirHandler.java new file mode 100644 index 0000000..1cec5a3 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/playish/TemplateDirHandler.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.playish; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class TemplateDirHandler implements DrumlinPlayishRouteHandler +{ + public TemplateDirHandler ( String dirInfo ) + { + } + + @Override + public void handle ( DrumlinRequestContext context, List args ) + throws IOException, + IllegalArgumentException, + IllegalAccessException, + InvocationTargetException + { + final String path = context.request ().getPathInContext (); + if ( path != null && path.length() > 0 ) + { + final String file = path.substring ( 1 ); + context.renderer ().renderTemplate ( file ); + } + else + { + throw new IOException ( "Couldn't render path." ); + } + } + + @Override + public boolean actionMatches ( String fullPath ) + { + return false; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/staticPaths/DrumlinStaticPathRouter.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/staticPaths/DrumlinStaticPathRouter.java new file mode 100644 index 0000000..85de08a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/framework/routing/staticPaths/DrumlinStaticPathRouter.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package com.att.nsa.drumlin.service.framework.routing.staticPaths; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.Map; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteInvocation; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRouteSource; +import com.att.nsa.drumlin.service.framework.routing.playish.StaticDirHandler; +import com.att.nsa.drumlin.service.framework.routing.playish.StaticFileHandler; +import com.att.nsa.drumlin.service.standards.HttpMethods; +import com.att.nsa.util.StreamTools; + +/** + * A static entry point routing source is a collection of routing entries for + * mapping request paths to static files and directories. + * + * @author peter@rathravane.com + */ +public class DrumlinStaticPathRouter implements DrumlinRouteSource +{ + public static String kMaxAge = StaticFileHandler.kMaxAge; + + public DrumlinStaticPathRouter ( File baseDir ) throws IOException + { + fBaseDir = baseDir.getCanonicalFile (); + if ( !fBaseDir.exists () || !fBaseDir.isDirectory () ) + { + throw new IllegalArgumentException ( baseDir + " is not a directory." ); + } + } + + /** + * This router will attempt to serve any path, assuming it's under the base + * directory. It handles GET/HEAD only, and rejects paths that are outside the base directory. + */ + @Override + public synchronized DrumlinRouteInvocation getRouteFor ( String verb, final String path ) + { + // only support GET (and HEAD) + if ( !verb.equalsIgnoreCase ( HttpMethods.GET ) && !verb.equalsIgnoreCase ( HttpMethods.HEAD ) ) + { + return null; + } + + final File toServe = new File ( fBaseDir, path ); + return new DrumlinRouteInvocation () + { + @Override + public void run ( DrumlinRequestContext context ) + throws IOException, + IllegalArgumentException, + IllegalAccessException, + InvocationTargetException + { + File in = toServe; + if ( in.isDirectory () ) + { + in = new File ( in, "index.html" ); + } + + final File canonical = in.getCanonicalFile (); + if ( !canonical.getAbsolutePath ().startsWith ( fBaseDir.getAbsolutePath () )) + { + log.debug ( "ignoring [" + path + "] because it is outside of the base directory." ); + log.warn ( "404 [" + path + "]==>[" + path + "] (" + in.getAbsolutePath () + ")" ); + context.response ().sendError ( 404, path + " was not found on this server." ); + return; + } + + // expiry. currently global. + final int cacheMaxAge = context.systemSettings ().getInt ( kMaxAge, -1 ); + if ( cacheMaxAge > 0 ) + { + context.response ().writeHeader ( "Cache-Control", "max-age=" + cacheMaxAge, true ); + } + + final String contentType = StaticDirHandler.mapToContentType ( in.getName () ); + + try + { + final FileInputStream is = new FileInputStream ( in ); + final OutputStream os = context.response ().getStreamForBinaryResponse ( contentType ); + StreamTools.copyStream ( is, os ); + } + catch ( FileNotFoundException e ) + { + log.warn ( "404 [" + path + "]==>[" + path + "] (" + in.getAbsolutePath () + ")" ); + context.response ().sendError ( 404, path + " was not found on this server." ); + } + catch ( IOException e ) + { + log.warn ( "500 [" + toServe.getAbsolutePath () + "]: " + e.getMessage () ); + context.response ().sendError ( 500, e.getMessage () ); + } + } + + @Override + public String getName () + { + return toServe.getPath (); + } + }; + } + + /** + * Reverse routing to entry points doesn't apply here. Always returns null. + */ + @Override + public String getRouteTo ( Class c, String staticMethodName, Map args ) + { + return null; + } + + private static final org.slf4j.Logger log = LoggerFactory.getLogger ( DrumlinStaticPathRouter.class ); + + private final File fBaseDir; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpMethods.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpMethods.java new file mode 100644 index 0000000..bc88c62 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpMethods.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.standards; + +/** + * HTTP methods as used in Drumlin. They're plain strings (rather than an + * enumeration). The HTTP spec allows for extension, so Drumlin does too. + */ +public class HttpMethods +{ + public static final String OPTIONS = "OPTIONS"; + public static final String GET = "GET"; + public static final String HEAD = "HEAD"; + public static final String POST = "POST"; + public static final String PUT = "PUT"; + public static final String DELETE = "DELETE"; + public static final String TRACE = "TRACE"; + public static final String CONNECT = "CONNECT"; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpStatusCodes.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpStatusCodes.java new file mode 100644 index 0000000..7b55cac --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/HttpStatusCodes.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.service.standards; + +public class HttpStatusCodes +{ + public static final int k100_continue = 100; + public static final int k101_switchingProtocols = 101; + + public static final int k200_ok = 200; + public static final int k201_created = 201; + public static final int k202_accepted = 202; + public static final int k203_nonAuthoritativeInformation = 203; + public static final int k204_noContent = 204; // HTTP/1.1: "MUST NOT include a message-body" + public static final int k205_resetContent = 205; // HTTP/1.1: "MUST NOT include an entity" + public static final int k206_partialContent = 206; + + public static final int k300_multipleChoices = 300; + public static final int k301_movedPermanently = 301; + public static final int k302_found = 302; + public static final int k303_seeOther = 303; + public static final int k304_notModified = 304; + public static final int k305_useProxy = 305; + public static final int k307_temporaryRedirect = 307; + + public static final int k400_badRequest = 400; + public static final int k401_unauthorized = 401; + public static final int k402_paymentRequired = 402; + public static final int k403_forbidden = 403; + public static final int k404_notFound = 404; + public static final int k405_methodNotAllowed = 405; + public static final int k406_notAcceptable = 406; + public static final int k407_proxyAuthReqd = 407; + public static final int k408_requestTimeout = 408; + public static final int k409_conflict = 409; + public static final int k410_gone = 410; + public static final int k411_lengthRequired = 411; + public static final int k412_preconditionFailed = 412; + public static final int k413_requestEntityTooLarge = 413; + public static final int k414_requestUriTooLong = 414; + public static final int k415_unsupportedMediaType = 415; + public static final int k416_requestedRangeNotSatisfiable = 416; + public static final int k417_expectationFailed = 417; + public static final int k428_preconditionRequired = 428; + public static final int k429_tooManyRequests = 429; + public static final int k431_requestHeaderFieldsTooLarge = 431; + + public static final int k500_internalServerError = 500; + public static final int k501_notImplemented = 501; + public static final int k502_badGateway = 502; + public static final int k503_serviceUnavailable = 503; + public static final int k504_gatewayTimeout = 504; + public static final int k505_httpVersionNotSupported = 505; + public static final int k511_networkAuthenticationRequired = 511; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/MimeTypes.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/MimeTypes.java new file mode 100644 index 0000000..29f1296 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/service/standards/MimeTypes.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.service.standards; + +/** + * Various commonly used MIME type constants. + * + * @author peter@rathravane.com + */ +public class MimeTypes +{ + public static final String kHtml = "text/html"; + public static final String kCss = "text/css"; + public static final String kPlainText = "text/plain"; + public static final String kCsv = "text/csv"; + + public static final String kAppGenericBinary = "application/octet-stream"; + public static final String kAppJavascript = "application/javascript"; + public static final String kAppJson = "application/json"; + public static final String kAppXml = "application/xml"; + + public static final String kFontEot = "application/vnd.ms-fontobject"; + public static final String kFontWoff = "application/x-font-woff"; + public static final String kFontTtf = "application/x-font-ttf"; + public static final String kFontOtf = "application/font-otf"; // typical, but not standardized (2013-07-10) + + public static final String kSvg = "image/svg+xml"; + public static final String kImagePng = "image/png"; + public static final String kImageGif = "image/gif"; + public static final String kImageJpg = "image/jpg"; + public static final String kImageIco = "image/vnd.microsoft.icon"; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/collections/rrMultiMap.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/collections/rrMultiMap.java new file mode 100644 index 0000000..ad57a82 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/collections/rrMultiMap.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.collections; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Maps a key to a list (not just a set) of values. + * @author peter + * + * @param + * @param + */ +public class rrMultiMap +{ + public rrMultiMap () + { + fMultiMap = new Hashtable> (); + } + + @Deprecated + public void add ( K k ) + { + put ( k ); + } + + @Deprecated + public void add ( K k, V v ) + { + put ( k, v ); + } + + @Deprecated + public void add ( K k, Collection v ) + { + put ( k, v ); + } + + public synchronized void put ( K k ) + { + getOrCreateFor ( k ); + } + + public synchronized void put ( K k, V v ) + { + LinkedList list = new LinkedList(); + list.add ( v ); + put ( k, list ); + } + + public synchronized void put ( K k, Collection v ) + { + List itemList = getOrCreateFor ( k ); + itemList.removeAll ( v ); // only one of a given value allowed + itemList.addAll ( v ); + } + + public synchronized void putAll ( Map> values ) + { + for ( Map.Entry> e : values.entrySet () ) + { + put ( e.getKey (), e.getValue () ); + } + } + + public synchronized boolean containsKey ( K k ) + { + return fMultiMap.containsKey ( k ); + } + + /** + * Get the values for a given key. A list is always returned, but it may be empty. + * @param k + * @return + */ + public synchronized List get ( K k ) + { + List itemList = new LinkedList (); + if ( fMultiMap.containsKey ( k ) ) + { + itemList = getOrCreateFor ( k ); + } + return itemList; + } + + /** + * Get the first value for the given key, or return null if none exists. + * @param k + * @return + */ + public V getFirst ( K k ) + { + final List items = get ( k ); + if ( items.size () > 0 ) + { + return items.get ( 0 ); + } + return null; + } + + public synchronized Collection getKeys () + { + return fMultiMap.keySet (); + } + + public synchronized Map> getValues () + { + return fMultiMap; + } + + public synchronized List remove ( K k ) + { + return fMultiMap.remove ( k ); + } + + public synchronized void remove ( K k, V v ) + { + List itemList = getOrCreateFor ( k ); + itemList.remove ( v ); + } + + public synchronized void clear () + { + fMultiMap.clear (); + } + + public synchronized int size () + { + return fMultiMap.size (); + } + + public synchronized int size ( K k ) + { + return getOrCreateFor ( k ).size (); + } + + private final Hashtable> fMultiMap; + + private synchronized List getOrCreateFor ( K k ) + { + List itemList = fMultiMap.get ( k ); + if ( itemList == null ) + { + itemList = new LinkedList (); + fMultiMap.put ( k, itemList ); + } + return itemList; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/cmdLinePrefs.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/cmdLinePrefs.java new file mode 100644 index 0000000..10e9f0b --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/cmdLinePrefs.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console; + +import java.util.Vector; + +import com.att.nsa.drumlin.till.nv.impl.nvWriteableTable; + +public class cmdLinePrefs extends nvWriteableTable +{ + public cmdLinePrefs ( rrCmdLineParser clp ) + { + super (); + + fParser = clp; + fLeftovers = new Vector (); + } + + /** + * get remaining arguments after the options are read + * @return a vector of args + */ + public Vector getFileArguments () + { + return fLeftovers; + } + + public String getFileArgumentsAsString () + { + final StringBuffer sb = new StringBuffer (); + for ( String s : fLeftovers ) + { + sb.append ( " " ); + sb.append ( s ); + } + return sb.toString().trim (); + } + + /** + * find out if an option was explicitly set by the caller + * @param optionWord + * @return true or false + */ + public boolean wasExplicitlySet ( String optionWord ) + { + return super.hasValueFor ( optionWord ); + } + + + public String getString ( String key ) throws missingReqdSetting + { + String result = null; + if ( wasExplicitlySet ( key ) ) + { + result = super.getString ( key ); + } + else + { + result = fParser.getArgFor ( key ); + } + + if ( result == null ) + { + throw new missingReqdSetting ( key ); + } + return result; + } + + private final rrCmdLineParser fParser; + private final Vector fLeftovers; + + void addLeftover ( String val ) + { + fLeftovers.add ( val ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/consoleLineReader.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/consoleLineReader.java new file mode 100644 index 0000000..6abc023 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/consoleLineReader.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import org.slf4j.LoggerFactory; + +public class consoleLineReader +{ + public static String getLine ( String prompt ) throws IOException + { + return sfReader.getLine ( prompt ); + } + + private interface reader + { + String getLine ( String prompt ) throws IOException; + } + + static + { + jline.console.ConsoleReader cr = null; + if ( Boolean.parseBoolean ( System.getProperty ( "rrJline", "true" ) ) ) + { + try + { + cr = new jline.console.ConsoleReader (); + } + catch ( IOException e ) + { + LoggerFactory.getLogger ( consoleLineReader.class ).warn ( "IOException initializing JLine. Falling back to standard Java I/O." ); + cr = null; + } + } + + if ( cr != null ) + { + final jline.console.ConsoleReader crf = cr; + sfReader = new reader () + { + @Override + public String getLine ( String prompt ) throws IOException + { + return crf.readLine ( prompt ); + } + }; + } + else + { + final BufferedReader br = new BufferedReader ( new InputStreamReader ( System.in ) ); + sfReader = new reader () + { + @Override + public String getLine ( String prompt ) throws IOException + { + System.out.print ( prompt ); + System.out.flush (); + return br.readLine (); + } + }; + } + } + + private static final reader sfReader; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrCmdLineParser.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrCmdLineParser.java new file mode 100644 index 0000000..3ef3881 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrCmdLineParser.java @@ -0,0 +1,395 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console; + +import java.util.HashMap; +import java.util.TreeSet; + +import com.att.nsa.drumlin.till.console.rrConsole.usageException; +import com.att.nsa.drumlin.till.data.rrConvertor; + +/** + * Assists in reading command line settings. + */ +public class rrCmdLineParser +{ + public rrCmdLineParser () + { + fSingleToWord = new HashMap (); + fWordsNeedingValues = new TreeSet (); + fOptions = new HashMap (); + fMinFiles = 0; + fMaxFiles = Integer.MAX_VALUE; + } + + /** + * Register a boolean option. + * @param word e.g. "force" + * @param singleChar e.g. "f". + * @param defValue + */ + public void registerOnOffOption ( String word, Character singleChar, boolean defValue ) + { + if ( word == null ) + { + throw new IllegalArgumentException ( "An option 'word' is required." ); + } + + if ( singleChar != null ) + { + fSingleToWord.put ( singleChar, word ); + } + + fOptions.put ( word, new onOff ( defValue ) ); + } + + public void registerOptionWithValue ( String word ) + { + registerOptionWithValue ( word, null, null, null ); + } + + /** + * register an option that takes a value + * @param word the full word for this option, e.g. "verbose" + * @param singleChar a single char representation of this option, e.g. "v". Can be null. + * @param defValue the default value for the option if none is provided + * @param allowed if not null, a limited range of values for the option + */ + public void registerOptionWithValue ( String word, String singleChar, String defValue, String[] allowed ) + { + if ( word == null ) + { + throw new IllegalArgumentException ( "An option 'word' is required." ); + } + + if ( singleChar != null ) + { + if ( singleChar.length () > 1 ) + { + throw new IllegalArgumentException ( singleChar + " is not a single character." ); + } + fSingleToWord.put ( singleChar.charAt ( 0 ), word ); + } + + fWordsNeedingValues.add ( word ); + fOptions.put ( word, new setting ( word, defValue, allowed ) ); + } + + /** + * allows no file arguments + */ + public void requireNoFileArguments () + { + requireFileArguments ( 0, 0 ); + } + + /** + * allows exactly one file argument + */ + public void requireOneFileArgument () + { + requireFileArguments ( 1, 1 ); + } + + /** + * sets the range for required file args from the given min to no max + * @param min + */ + public void requireMinFileArguments ( int min ) + { + requireFileArguments ( min, Integer.MAX_VALUE ); + } + + /** + * require a specific number of file arguments + * @param exactly + */ + public void requireFileArguments ( int exactly ) + { + requireFileArguments ( exactly, exactly ); + } + + /** + * set a range for file arg count for the parser + * @param min 0 or higher + * @param max 0 or higher, use Integer.MAX_VALUE for no max + */ + public void requireFileArguments ( int min, int max ) + { + fMinFiles = min; + fMaxFiles = max; + } + + /** + * find out if an option has a value (or is just on/off) + * @param optionWord + * @return the value for the option + */ + public boolean hasArg ( String optionWord ) + { + return ( fOptions.get ( optionWord ) != null ); + } + + /** + * find out if a boolean option has been set + * @param optionWord + * @return true if the value for the option is set + */ + public boolean isSet ( String optionWord ) + { + final option o = fOptions.get ( optionWord ); + return ( o != null ) ? rrConvertor.convertToBoolean (o.getDefault()) : false; + } + + /** + * get the default value for a given option + * @param optionWord + * @return the default value for the option + */ + public String getArgFor ( String optionWord ) + { + final option o = fOptions.get ( optionWord ); + return ( o != null ) ? o.getDefault () : ""; + } + + /** + * reads command line arguments + * @param args + */ + public cmdLinePrefs processArgs ( String[] args ) throws usageException + { + final cmdLinePrefs prefs = new cmdLinePrefs ( this ); + + boolean seenDashDash = false; + int i=0; + for ( ; i fMaxFiles ) + { + throw new usageException ( getErrorMsgForWrongCount ( leftoverCount ) ); + } + + return prefs; + } + + private static String plural ( String word, int count ) + { + return ( count == 1 ? word : word + "s" ); + } + + private String getErrorMsgForWrongCount ( int count ) + { + if ( fMinFiles == fMaxFiles ) + { + if ( fMinFiles == 0 ) + { + return "You may not provide any arguments."; + } + else + { + return "You must provide " + fMinFiles + " " + plural("argument",fMinFiles) + "."; + } + } + else + { + final String minPart = ( fMinFiles <= 0 ? null : "at least " + fMinFiles + " " + plural("argument",fMinFiles) ); + final String maxPart = ( fMaxFiles == Integer.MAX_VALUE ? null : "at most " + fMaxFiles + " " + plural("argument",fMaxFiles) ); + + return "You must provide " + + ( minPart != null ? minPart : "" ) + + ( minPart != null && maxPart != null ? " and " : "" ) + + ( maxPart != null ? maxPart : "" ) + + "."; + } + } + + private boolean reqsValue ( String optWord ) + { + return fWordsNeedingValues.contains ( optWord ); + } + + private void handleOption ( cmdLinePrefs prefs, String optWord ) throws usageException + { + handleOption ( prefs, optWord, Boolean.TRUE.toString () ); + } + + private void handleOption ( cmdLinePrefs prefs, String optWord, String value ) throws usageException + { + final option o = fOptions.get ( optWord ); + if ( o == null ) + { + throw new usageException ( "Unrecognized option " + optWord ); + } + + final String valToUse = o.checkValue ( value ); + prefs.set ( optWord, valToUse ); + } + + private interface option + { + String checkValue ( String val ) throws usageException; + String getDefault (); + } + + private class onOff implements option + { + public onOff ( boolean defVal ) + { + fValue = defVal; + } + + @Override + public String checkValue ( String val ) + { + return "" + rrConvertor.convertToBooleanBroad ( val ); + } + + @Override + public String getDefault () + { + return Boolean.toString ( fValue ); + } + + private final boolean fValue; + } + + private class setting implements option + { + public setting ( String name, String defVal, String[] allowed ) + { + fSetting = name; + fValue = defVal; + fAllowed = allowed; + } + + @Override + public String checkValue ( String val ) throws usageException + { + boolean canSet = true; + if ( fAllowed != null ) + { + canSet = false; + for ( String a : fAllowed ) + { + if ( a.equals ( val ) ) + { + canSet = true; + break; + } + } + if ( !canSet ) + { + throw new usageException ( "Value " + val + " is not allowed for setting " + fSetting ); + } + } + return val; + } + + @Override + public String getDefault () + { + return fValue; + } + + private final String fSetting; + private String fValue; + private String[] fAllowed; + } + + private final HashMap fSingleToWord; + private final TreeSet fWordsNeedingValues; + private final HashMap fOptions; + private int fMinFiles; + private int fMaxFiles; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrConsole.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrConsole.java new file mode 100644 index 0000000..a1eafdf --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/rrConsole.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Pattern; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.rrNvWriteable; +import com.att.nsa.drumlin.till.nv.impl.nvInstallTypeWrapper; +import com.att.nsa.drumlin.till.nv.impl.nvReadableStack; +import com.att.nsa.drumlin.till.nv.impl.nvWriteableTable; + +/** + * A console program runs on the command line. + *

+ * The console class expects the program's main() routine to call its + * runFromMain() method to start the system. + * + * @author peter + */ +public class rrConsole +{ + public static class usageException extends Exception + { + public usageException ( String correctUsage ) { super(correctUsage); } + public usageException ( Exception cause ) { super(cause); } + private static final long serialVersionUID = 1L; + } + + public static class startupFailureException extends Exception + { + public startupFailureException ( Exception x ) { super(x); } + public startupFailureException ( String msg ) { super(msg); } + public startupFailureException ( String msg, Exception x ) { super(msg,x); } + private static final long serialVersionUID = 1L; + } + + /** + * A looper is an object that is run repeatedly (in a loop). This class is + * what the main thread of the program does between startup and exit. + * + * @author peter + * + */ + public interface looper + { + /** + * setup the looper and return true to continue. Called once. + * @param p + * @param cmdLine + * @return true/false + */ + boolean setup ( rrNvReadable prefs, cmdLinePrefs cmdLine ); + + /** + * run a loop iteration, return true to continue, false to exit + * @return true to continue, false to exit + */ + boolean loop ( rrNvReadable prefs ); + + /** + * teardown the looper. called once. + * @param p + */ + void teardown ( rrNvReadable prefs ); + } + + protected rrCmdLineParser getCmdLineParser () + { + return fCmdLineParser; + } + + protected rrConsole () + { + fDefaults = new nvWriteableTable (); + fCmdLineParser = new rrCmdLineParser (); + } + + public void runFromMain ( String[] args ) throws Exception + { + // get setup + installShutdownHook (); + setupDefaults ( fDefaults ); + setupOptions ( fCmdLineParser ); + + // parse the command line + final cmdLinePrefs cmdLine = fCmdLineParser.processArgs ( args ); + + // build a preferences stack + final nvReadableStack stack = new nvReadableStack (); + stack.push ( fDefaults ); + stack.push ( cmdLine ); + + // optionally load more configuration + final nvInstallTypeWrapper wrapper = new nvInstallTypeWrapper ( stack ); + final rrNvReadable config = loadAdditionalConfig ( wrapper ); + if ( config != null ) + { + stack.pushBelow ( config, cmdLine ); + } + + // init and get the run loop + final looper l = init ( wrapper, cmdLine ); + if ( l != null ) + { + if ( l.setup ( wrapper, cmdLine ) ) + { + while ( l.loop ( wrapper ) ) {} + l.teardown ( wrapper ); + } + } + + cleanup (); + } + + /** + * Override this to handle an abrupt shutdown. This method is called when the system exits. + */ + protected void onAbruptShutdown () { } + + /** + * Override this to setup default settings for the program. + * @param pt + */ + protected void setupDefaults ( rrNvWriteable pt ) {} + + /** + * Override this to setup recognized command line options. + * @param p + */ + protected void setupOptions ( rrCmdLineParser p ) {} + + /** + * Override this to load additional configuration. If a non-null config is returned, + * it's inserted into the preferences stack between the default settings and the command line + * settings. That way, the command line arguments have precedence. + * @param currentPrefs + * @throws loadException + * @throws missingReqdSetting + */ + protected rrNvReadable loadAdditionalConfig ( rrNvReadable currentPrefs ) throws rrNvReadable.loadException, rrNvReadable.missingReqdSetting { return null; } + + /** + * Init the program and return a loop instance if the program should continue. The base + * class returns null, so you have to override this to do anything beyond init. + * @param p + * @return non-null to continue, null to exit + * @throws missingReqdSetting + * @throws invalidSettingValue + */ + protected looper init ( rrNvReadable p, cmdLinePrefs cmdLine ) throws rrNvReadable.missingReqdSetting, rrNvReadable.invalidSettingValue, startupFailureException { return null; } + + /** + * Override this to run any cleanup code after the main loop. + */ + protected void cleanup () {} + + /** + * expand a file argument ("*" matches, etc.) + * @param arg + * @return + * @throws FileNotFoundException + */ + protected List expandFileArg ( String arg ) throws FileNotFoundException + { + final LinkedList fileList= new LinkedList (); + + final File file = new File ( arg ); + final File parentDir = file.getParentFile (); + if ( parentDir != null ) + { + final String matchPart = file.getName ().replace ( "*", ".*" ); // cmd line regex to java regex + final Pattern p = Pattern.compile ( matchPart ); + + final File[] files = parentDir.listFiles ( new FilenameFilter () + { + @Override + public boolean accept ( File dir, String name ) + { + return p.matcher ( name ).matches (); + } + } ); + + if ( files != null ) + { + for ( File f : files ) + { + fileList.add ( f ); + } + } + } + return fileList; + } + + private final rrCmdLineParser fCmdLineParser; + private final nvWriteableTable fDefaults; + + private void installShutdownHook () + { + Runtime.getRuntime ().addShutdownHook ( + new Thread () + { + @Override + public void run () + { + onAbruptShutdown (); + } + } + ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/command.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/command.java new file mode 100644 index 0000000..3a3bf5c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/command.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console.shell; + +import java.io.PrintStream; +import java.util.HashMap; + +import com.att.nsa.drumlin.till.console.rrConsole.usageException; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public interface command +{ + String getCommand (); + + /** + * check the arguments provided + * @param args + */ + void checkArgs ( rrNvReadable p, String[] args ) throws usageException; + + /** + * @return a string used for the help command to show simple usage + */ + String getUsage (); + + /** + * @return a string used for the help command to show detail info + */ + String getHelp (); + + /** + * @param outTo + * @return true to continue, false to exit + * @throws com.rathravane.rrConsole.ui.console2.console.usageException + */ + consoleLooper.inResult execute ( HashMap workspace, PrintStream outTo ) throws usageException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/commandList.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/commandList.java new file mode 100644 index 0000000..c651408 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/commandList.java @@ -0,0 +1,15 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console.shell; + +public interface commandList +{ + /** + * return the command for a text command, or null + * @param cmd + * @return information about the command, or null + */ + command getCommandFor ( String cmd ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/consoleLooper.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/consoleLooper.java new file mode 100644 index 0000000..f6836eb --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/consoleLooper.java @@ -0,0 +1,418 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console.shell; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.TreeSet; +import java.util.Vector; + +import com.att.nsa.drumlin.till.console.rrConsole; +import com.att.nsa.drumlin.till.console.consoleLineReader; +import com.att.nsa.drumlin.till.console.cmdLinePrefs; +import com.att.nsa.drumlin.till.console.rrConsole.usageException; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * implements a looper that repeatedly prompts for a command line and handles it + * @author peter + */ +public class consoleLooper implements rrConsole.looper +{ + public consoleLooper ( String[] headerLines, String prompt, String secondaryPrompt, commandList cl ) + { + fHeaderLines = headerLines; + fInputQueue = new LinkedList (); + fPrompt = prompt; + fSecondaryPrompt = secondaryPrompt; + fEnableHelp = true; + fWorkspace = new HashMap (); + fState = inResult.kReady; + fCommands = cl; + } + + public enum inResult + { + kReady, + kSecondaryPrompt, + kQuit + }; + + /** + * this key is used for a boolean setting that will suppress the copyright notice + */ + public static final String kSetting_Quiet = "quiet"; + + @Override + public boolean setup ( rrNvReadable p, cmdLinePrefs clp ) + { + boolean quiet = p.getBoolean ( kSetting_Quiet, false ); + + if ( clp != null ) + { + final String args = clp.getFileArgumentsAsString (); + if ( args != null && args.length () > 0 ) + { + queue ( args ); + queue ( "quit" ); + quiet = true; + } + } + + if ( !quiet ) + { + writeHeader (); + } + + return true; + } + + @Override + public void teardown ( rrNvReadable p ) + { + } + + @Override + public boolean loop ( rrNvReadable p ) + { + boolean result = true; + try + { + final String line = getInput (); + fState = handleInput ( p, line, System.out ); + if ( fState == null ) + { + fState = inResult.kReady; + } + } + catch ( IOException x ) + { + // a break in console IO, we're done + System.err.println ( x.getMessage () ); + result = false; + } + return result && !fState.equals ( inResult.kQuit ); + } + + public synchronized void queue ( String input ) + { + fInputQueue.add ( input ); + } + + public synchronized void queueFromCmdLine ( cmdLinePrefs clp, boolean withQuit ) + { + final Vector args = clp.getFileArguments (); + if ( args.size () > 0 ) + { + final StringBuffer sb = new StringBuffer (); + for ( String s : args ) + { + sb.append ( s ); + sb.append ( ' ' ); + } + queue ( sb.toString () ); + if ( withQuit ) queue ( "quit" ); + } + } + + protected void writeHeader () + { + if ( fHeaderLines != null ) + { + for ( String header : fHeaderLines ) + { + System.out.println ( header ); + } + } + } + + /** + * override this to handle input before its been parsed in the usual way + * @param input + * @param outputTo + * @return true to continue, false to exit + */ + protected inResult handleInput ( rrNvReadable p, String input, PrintStream outputTo ) + { + inResult result = inResult.kReady; + + final String[] commandLine = splitLine ( input ); + if ( commandLine.length == 0 ) + { + result = handleEmptyLine ( p, outputTo ); + } + else + { + result = handleCommand ( p, commandLine, outputTo ); + } + + return result; + } + + /** + * default handling for empty lines -- just ignore them + * @return true + */ + protected inResult handleEmptyLine ( rrNvReadable p, PrintStream outputTo ) + { + return inResult.kReady; + } + + /** + * consoles can override this to change how command lines are processed + * @param commandLine + * @param outputTo + * @return true to continue, false to exit + */ + protected inResult handleCommand ( rrNvReadable p, String[] commandLine, PrintStream outputTo ) + { + inResult result = inResult.kReady; + final command m = getHandler ( commandLine ); + if ( m != null ) + { + final int argsLen = commandLine.length - 1; + final String[] args = new String [ argsLen ]; + System.arraycopy ( commandLine, 1, args, 0, argsLen ); + + try + { + result = invoke ( m, p, args, outputTo ); + } + catch ( Exception x ) + { + result = handleInvocationException ( commandLine, x, outputTo ); + } + } + else + { + result = handleUnrecognizedCommand ( commandLine, outputTo ); + } + return result; + } + + protected inResult invoke ( command m, rrNvReadable p, String[] args, PrintStream outputTo ) + { + try + { + m.checkArgs ( p, args ); + return m.execute ( fWorkspace, outputTo ); + } + catch ( usageException x ) + { + outputTo.println ( m.getUsage () ); + outputTo.println ( x.getMessage () ); + } + return inResult.kReady; + } + + /** + * default handling for unrecognized commands + * @return inResult.kReady + */ + protected inResult handleUnrecognizedCommand ( String[] commandLine, PrintStream outputTo ) + { + outputTo.println ( "Unrecognized command '" + commandLine[0] + "'." ); + return inResult.kReady; + } + + /** + * default handling for invocation problems + * @return inResult.kReady + */ + protected inResult handleInvocationException ( String[] commandLine, Exception x, PrintStream outputTo ) + { + if ( x instanceof InvocationTargetException ) + { + InvocationTargetException itc = (InvocationTargetException) x; + Throwable target = itc.getTargetException (); + outputTo.println ( "ERROR: " + target.getClass ().getName () + ": " + target.getMessage () ); + } + else + { + outputTo.println ( "Error running command '" + commandLine[0] + "'. " + x.getMessage() ); + } + return inResult.kReady; + } + + protected HashMap getWorkspace () + { + return fWorkspace; + } + + private String[] fHeaderLines; + private final LinkedList fInputQueue; + private final String fPrompt; + private final String fSecondaryPrompt; + private boolean fEnableHelp; + private inResult fState; + private final commandList fCommands; + private final HashMap fWorkspace; + + public static final String kCmdPrefix = "__"; + public static final int kCmdPrefixLength = kCmdPrefix.length (); + + private synchronized String getInput () throws IOException + { + String input = null; + if ( fInputQueue.size () > 0 ) + { + input = fInputQueue.remove (); + } + else + { + String prompt = fPrompt; + if ( fState == inResult.kReady ) + { + System.out.println (); + } + else + { + prompt = fSecondaryPrompt; + } + + input = consoleLineReader.getLine ( prompt ); + if ( input == null ) + { + input = ""; + } + } + return input; + } + + /** + * split a string on its whitespace into individual tokens + * @param line + * @return split array + */ + static String[] splitLine ( final String line ) + { + final LinkedList tokens = new LinkedList (); + + StringBuffer current = new StringBuffer (); + boolean quoting = false; + for ( int i=0; i 0 ) + { + tokens.add ( current.toString () ); + } + current = new StringBuffer (); + } + else if ( c == '"' ) + { + // if we see "abc"def, that's "abc", then "def" + if ( current.length () == 0 ) + { + // starting quoted string. eat it, flip quote flag + quoting = true; + } + else if ( !quoting ) + { + // abc"def + tokens.add ( current.toString () ); + current = new StringBuffer (); + quoting = true; + } + else + { + // end quoted string + tokens.add ( current.toString () ); + current = new StringBuffer (); + quoting = false; + } + } + else + { + current.append ( c ); + } + } + if ( current.length () > 0 ) + { + tokens.add ( current.toString () ); + } + + return tokens.toArray ( new String[ tokens.size () ] ); + } + + private command getHandler ( String[] cmdLine ) + { + if ( cmdLine.length > 0 ) + { + return fCommands.getCommandFor ( cmdLine[0] ); + } + else + { + return null; + } + } + + public void __script ( String[] args, PrintStream outTo ) throws rrConsole.usageException, IOException + { + if ( args.length != 2 ) + { + throw new rrConsole.usageException ( "script " ); + } + + LinkedList lines = new LinkedList (); + final String filename = args[1]; + final BufferedReader bis = new BufferedReader ( new FileReader ( filename ) ); + String input; + while ( ( input = bis.readLine () ) != null ) + { + lines.add ( input ); + } + bis.close (); + + // add to the front of the queue so that script within script runs in + // correct order. + synchronized ( this ) + { + fInputQueue.addAll ( 0, lines ); + } + } + + public void __help ( String[] args, PrintStream outTo ) throws rrConsole.usageException, IOException + { + if ( fEnableHelp ) + { + TreeSet allMethods = new TreeSet (); + + Class clazz = getClass (); + while ( !clazz.equals ( Object.class ) ) + { + Method[] methods = clazz.getDeclaredMethods (); + for ( Method m : methods ) + { + final String methodName = m.getName (); + if ( methodName.startsWith ( kCmdPrefix ) && + methodName.length () > kCmdPrefixLength ) + { + Class[] params = m.getParameterTypes (); + if ( params.length == 2 && params[0].equals ( String[].class ) && + params[1].equals ( PrintStream.class ) ) + { + allMethods.add ( methodName.substring ( kCmdPrefixLength ) ); + } + } + } + clazz = clazz.getSuperclass (); + } + + for ( String s : allMethods ) + { + outTo.println ( " " + s ); + } + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/simpleCommand.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/simpleCommand.java new file mode 100644 index 0000000..533a5b9 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/simpleCommand.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console.shell; + +import java.io.PrintStream; +import java.util.HashMap; + +import com.att.nsa.drumlin.till.console.rrCmdLineParser; +import com.att.nsa.drumlin.till.console.cmdLinePrefs; +import com.att.nsa.drumlin.till.console.rrConsole.usageException; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public abstract class simpleCommand implements command +{ + protected simpleCommand ( String cmd ) + { + this ( cmd, cmd, null ); + } + + protected simpleCommand ( String cmd, String usage ) + { + this ( cmd, usage, null ); + } + + protected simpleCommand ( String cmd, String usage, String help ) + { + fCmd = cmd; + fUsage = usage; + fHelp = help; + fArgsParser = new rrCmdLineParser (); + fPrefs = null; + fEnabled = true; + } + + public void enable ( boolean e ) + { + fEnabled = e; + } + + public boolean enabled () + { + return fEnabled; + } + + @Override + public final void checkArgs ( rrNvReadable basePrefs, String[] args ) throws usageException + { + setupParser ( fArgsParser ); + fPrefs = fArgsParser.processArgs ( args ); + } + + @Override + public String getCommand () { return fCmd; } + + @Override + public String getUsage () { return fUsage; } + + @Override + public String getHelp () { return fHelp; } + + @Override + public final consoleLooper.inResult execute ( HashMap workspace, PrintStream outTo ) throws usageException + { + try + { + return execute ( workspace, fPrefs, outTo ); + } + catch ( rrNvReadable.missingReqdSetting e ) + { + throw new usageException ( e ); + } + } + + /** + * Override this to run the command. + * @param prefs + * @param outTo + * @return true to continue, false to exit + * @throws com.rathravane.rrConsole.ui.console2.console.usageException + * @throws missingReqdSetting + */ + protected abstract consoleLooper.inResult execute ( HashMap workspace, cmdLinePrefs p, PrintStream outTo ) throws usageException, rrNvReadable.missingReqdSetting; + + /** + * override this to specify arguments for the command + * @param clp + */ + protected void setupParser ( rrCmdLineParser clp ) {} + + private final String fCmd; + private final String fUsage; + private final String fHelp; + + private final rrCmdLineParser fArgsParser; + private cmdLinePrefs fPrefs; + private boolean fEnabled; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/stdCommandList.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/stdCommandList.java new file mode 100644 index 0000000..6163a67 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/console/shell/stdCommandList.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.console.shell; + +import java.io.PrintStream; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; + +import com.att.nsa.drumlin.till.console.rrCmdLineParser; +import com.att.nsa.drumlin.till.console.cmdLinePrefs; + +public class stdCommandList implements commandList +{ + public stdCommandList () + { + this ( true ); + } + + public stdCommandList ( boolean withStdCommands ) + { + fCommands = new HashMap (); + if ( withStdCommands ) + { + addStandardCommands (); + } + } + + public void registerCommand ( command c ) + { + fCommands.put ( c.getCommand (), c ); + } + + public void removeCommand ( String key ) + { + fCommands.remove ( key ); + } + + public void clearCommands () + { + fCommands.clear (); + } + + public void addStandardCommands () + { + registerCommand ( new simpleCommand ( "exit" ) + { + @Override + public consoleLooper.inResult execute ( HashMap workspace, cmdLinePrefs prefs, PrintStream outTo ) { return consoleLooper.inResult.kQuit; } + } ); + registerCommand ( new simpleCommand ( "quit" ) + { + @Override + public consoleLooper.inResult execute ( HashMap workspace, cmdLinePrefs prefs, PrintStream outTo ) { return consoleLooper.inResult.kQuit; } + } ); + registerCommand ( new simpleCommand ( "help", "help []" ) + { + @Override + protected void setupParser ( rrCmdLineParser clp ) + { + clp.requireFileArguments ( 0, 1 ); + } + + @Override + public consoleLooper.inResult execute ( HashMap workspace, cmdLinePrefs prefs, PrintStream outTo ) + { + if ( prefs.getFileArguments ().size() == 1 ) + { + final String cmdText = prefs.getFileArguments ().firstElement (); + final command cmd = fCommands.get ( cmdText ); + if ( cmd != null ) + { + outTo.println ( " " + cmd.getUsage () ); + final String help = cmd.getHelp (); + if ( help != null ) + { + outTo.println (); + outTo.println ( help ); + } + } + else + { + outTo.println ( "Unknown command: " + cmdText + "." ); + } + } + else + { + outTo.println ( "The available commands are:" ); + outTo.println (); + + final LinkedList commands = new LinkedList (); + commands.addAll ( getAllCommands () ); + Collections.sort ( commands ); + for ( String cmd : commands ) + { + outTo.println ( " " + cmd ); + } + + outTo.println (); + outTo.println ( "You can type 'help ' to get more info on that command." ); + } + return consoleLooper.inResult.kReady; + } + } ); + } + + public Collection getAllCommands () + { + return fCommands.keySet (); + } + + @Override + public command getCommandFor ( String cmd ) + { + return fCommands.get ( cmd ); + } + + private final HashMap fCommands; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64Constants.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64Constants.java new file mode 100644 index 0000000..e94b36f --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64Constants.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data.base64; + +public class rrcBase64Constants +{ + public static final char kNewline = 10; + + public static char[] nibblesToB64 = new char [64]; + public static byte[] b64ToNibbles = new byte [128]; + + static + { + int j = 0; + for ( char c = 'A'; c <= 'Z'; c++ ) + { + nibblesToB64[j++] = c; + } + for ( char c = 'a'; c <= 'z'; c++ ) + { + nibblesToB64[j++] = c; + } + for ( char c = '0'; c <= '9'; c++ ) + { + nibblesToB64[j++] = c; + } + nibblesToB64[j++] = '+'; + nibblesToB64[j++] = '/'; + + for ( int i = 0; i < b64ToNibbles.length; i++ ) + { + b64ToNibbles[i] = -1; + } + for ( int i = 0; i < 64; i++ ) + { + b64ToNibbles[nibblesToB64[i]] = (byte) i; + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64InputStream.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64InputStream.java new file mode 100644 index 0000000..5c4ee16 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64InputStream.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data.base64; + +import java.io.IOException; +import java.io.InputStream; +import java.util.LinkedList; + +public class rrcBase64InputStream extends InputStream +{ + public rrcBase64InputStream ( InputStream upstream ) + { + fUpstream = upstream; + fPendingOutput = new LinkedList (); + fEndOfStream = false; + } + + private final LinkedList fPendingOutput; + + @Override + public int available () throws IOException + { + fillBuffer (); + return fPendingOutput.size (); + } + + @Override + public int read () + throws IOException + { + int result = -1; + + fillBuffer (); + if ( fPendingOutput.size () > 0 ) + { + result = fPendingOutput.remove (); + if ( result < 0 ) + { + result += 256; + } + } + else if ( !fEndOfStream ) + { + result = 0; + } + return result; + } + + private byte[] decode ( char[] in ) + { + int iLen = in.length; + if ( iLen % 4 != 0 ) + throw new IllegalArgumentException ( + "Length of Base64 encoded input string is not a multiple of 4." ); + while ( iLen > 0 && in[iLen - 1] == '=' ) + iLen--; + int oLen = ( iLen * 3 ) / 4; + byte[] out = new byte [oLen]; + int ip = 0; + int op = 0; + while ( ip < iLen ) + { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iLen ? in[ip++] : 'A'; + int i3 = ip < iLen ? in[ip++] : 'A'; + if ( i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127 ) + throw new IllegalArgumentException ( + "Illegal character in Base64 encoded data." ); + int b0 = rrcBase64Constants.b64ToNibbles[i0]; + int b1 = rrcBase64Constants.b64ToNibbles[i1]; + int b2 = rrcBase64Constants.b64ToNibbles[i2]; + int b3 = rrcBase64Constants.b64ToNibbles[i3]; + if ( b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0 ) + throw new IllegalArgumentException ( + "Illegal character in Base64 encoded data." ); + int o0 = ( b0 << 2 ) | ( b1 >>> 4 ); + int o1 = ( ( b1 & 0xf ) << 4 ) | ( b2 >>> 2 ); + int o2 = ( ( b2 & 3 ) << 6 ) | b3; + out[op++] = (byte) o0; + if ( op < oLen ) + out[op++] = (byte) o1; + if ( op < oLen ) + out[op++] = (byte) o2; + } + return out; + } + + private final InputStream fUpstream; + private boolean fEndOfStream; + + private void fillBuffer () throws IOException + { + if ( fPendingOutput.size () == 0 ) + { + // skip any newlines + int first = fUpstream.read (); + while ( first == rrcBase64Constants.kNewline ) + { + first = fUpstream.read (); + } + + if ( first == -1 ) + { + fEndOfStream = true; + } + else + { + // read 3 more bytes + byte[] next3 = new byte[3]; + int readFromNext3 = fUpstream.read ( next3 ); + if ( readFromNext3 != 3 ) + { + throw new IOException ( "rrcBase64InputStream expects 4 bytes from its underlying stream but got " + + (1+readFromNext3) ); + } + + char[] block = new char[4]; + block[0] = (char)((byte) first); + for ( int i=0; i<3; i++ ) { block[i+1]=(char)next3[i]; } + + byte[] decoded = decode ( block ); + for ( byte b : decoded ) + { + fPendingOutput.add ( b ); + } + } + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64OutputStream.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64OutputStream.java new file mode 100644 index 0000000..c3159d6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/base64/rrcBase64OutputStream.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data.base64; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.LinkedList; + +public class rrcBase64OutputStream + extends OutputStream +{ + public rrcBase64OutputStream ( OutputStream downstream ) + { + fDownstream = downstream; + fPendings = new LinkedList (); + fWrittenToLine = 0; + } + + @Override + public void write ( int b ) + throws IOException + { + fPendings.add ( (byte) b ); + writePendings ( false ); + } + + @Override + public void close () throws IOException + { + writePendings ( true ); + fDownstream.close (); + } + + private int writeNow () + { + int result = 0; + int pending = fPendings.size (); + if ( pending > kBufferSize ) + { + result = kBufferSize; + } + return result; + } + + private void writePendings ( boolean pad ) throws IOException + { + int thisWrite = fPendings.size (); + if ( !pad ) + { + thisWrite = writeNow (); + } + + if ( thisWrite > 0 ) + { + byte[] bb = new byte [ thisWrite ]; + for ( int i=0; i>> 2; + int o1 = ( ( i0 & 3 ) << 4 ) | ( i1 >>> 4 ); + int o2 = ( ( i1 & 0xf ) << 2 ) | ( i2 >>> 6 ); + int o3 = i2 & 0x3F; + out[op++] = rrcBase64Constants.nibblesToB64[o0]; + out[op++] = rrcBase64Constants.nibblesToB64[o1]; + out[op] = op < oDataLen ? rrcBase64Constants.nibblesToB64[o2] : '='; + op++; + out[op] = op < oDataLen ? rrcBase64Constants.nibblesToB64[o3] : '='; + op++; + } + return out; + } + + private static final int kMaxLine = 80; + private static final int kBufferSize = 3*64; // multiple of 3 for no padding + + private OutputStream fDownstream; + private int fWrittenToLine; + private final LinkedList fPendings; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/humanReadableHelper.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/humanReadableHelper.java new file mode 100644 index 0000000..71acfc0 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/humanReadableHelper.java @@ -0,0 +1,273 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class humanReadableHelper +{ + private static final long kKilobyte = 1024; + private static final long kMegabyte = 1024 * kKilobyte; + private static final long kGigabyte = 1024 * kMegabyte; + private static final long kTerabyte = 1024 * kGigabyte; + private static final long kPetabyte = 1024 * kTerabyte; + private static final long kExabyte = 1024 * kPetabyte; + + public static String byteCountValue ( long inBytes ) + { + String result = "" + inBytes + " bytes"; + if ( inBytes > kExabyte ) + { + double d = inBytes / kExabyte; + result = "" + d + " EB"; + } + else if ( inBytes > kPetabyte ) + { + double d = inBytes / kPetabyte; + result = "" + d + " PB"; + } + else if ( inBytes > kTerabyte ) + { + double d = inBytes / kTerabyte; + result = "" + d + " TB"; + } + else if ( inBytes > kGigabyte ) + { + double d = inBytes / kGigabyte; + result = "" + d + " GB"; + } + else if ( inBytes > kMegabyte ) + { + double d = inBytes / kMegabyte; + result = "" + d + " MB"; + } + else if ( inBytes > kKilobyte ) + { + double d = inBytes / kKilobyte; + result = "" + d + " KB"; + } + return result; + } + + @Deprecated + public static String memoryValue ( long inBytes ) + { + return byteCountValue ( inBytes ); + } + + public static final long kSecond = 1000; + public static final long kMinute = 60 * kSecond; + public static final long kHour = 60 * kMinute; + public static final long kDay = 24 * kHour; + public static final long kWeek = 7 * kDay; + public static final long kMonth = 30 * kDay; + public static final long kYear = 365 * kDay; + + public static String numberValue ( long units ) + { + final StringBuffer sb = new StringBuffer (); + + final String raw = "" + units; + final int count = raw.length (); + final int firstPart = count % 3; + int printed = 3 - firstPart; + for ( int i=0; i 0 ) + { + sb.append ( ',' ); + } + printed = 0; + } + sb.append ( raw.charAt ( i ) ); + printed++; + } + + return sb.toString (); + } + + public static String ratioValue ( double d ) + { + // FIXME: use formatter + double rounded2 = Math.round ( d * 100 ) / 100.0; + return "" + rounded2; + } + + public static String pctValue ( double d ) + { + // FIXME: use formatter + final long pct = Math.round ( d * 100 ); + return "" + pct + "%"; + } + + public static String dateValue ( Date d ) + { + return sdf.format ( d ); + } + private static final SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy.MM.dd HH:mm:ss z" ); + + public static String elapsedTimeSince ( Date d ) + { + // return elapsed time with precision that's scaled back as the time grows distant + long unit = 1; + final long elapsedMs = System.currentTimeMillis () - d.getTime (); + + // over 5 seconds, report in seconds + if ( elapsedMs > 1000 * 5 ) + { + unit = 1000; + } + + // over 5 minutes, report in minutes + if ( elapsedMs > 1000*60*5 ) + { + unit = 1000 * 60; + } + + // over 5 hours, report in hours + if ( elapsedMs > 1000*60*60*5 ) + { + unit = 1000 * 60 * 60; + } + + // over 5 days, report in days + if ( elapsedMs > 1000*60*60*24*5 ) + { + unit = 1000 * 60 * 60 * 24; + } + + // over 5 weeks, report in weeks + if ( elapsedMs > 1000*60*60*24*7*5 ) + { + unit = 1000 * 60 * 60 * 24 * 7; + } + + // over 5 months, report in months + if ( elapsedMs > 1000*60*60*24*30*5 ) + { + unit = 1000 * 60 * 60 * 24 * 30; + } + + // over 2 years, report in years + if ( elapsedMs > 1000*60*60*24*365*2 ) + { + unit = 1000 * 60 * 60 * 24 * 365; + } + + return elapsedTimeSince ( d, unit ); + } + + public static String elapsedTimeSince ( Date d, long smallestUnitInMillis ) + { + if ( d == null ) + { + return ""; + } + + final Date now = new Date (); + final long elapsedMs = now.getTime () - d.getTime (); + if ( elapsedMs < 0 ) + { + return timeValue ( elapsedMs * -1, TimeUnit.MILLISECONDS, smallestUnitInMillis ) + " in the future"; + } + else + { + return timeValue ( elapsedMs, TimeUnit.MILLISECONDS, smallestUnitInMillis ) + " ago"; + } + } + + public static String timeValue ( long units, TimeUnit tu, long smallestUnit ) + { + final long timeInMs = TimeUnit.MILLISECONDS.convert ( units, tu ); + + String result = "" + timeInMs + " ms"; + if ( timeInMs > kYear ) + { + final long years = timeInMs / kYear; + final long remaining = timeInMs - ( years * kYear ); + result = "" + years + " yrs"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kMonth ) + { + final long months = timeInMs / kMonth; + final long remaining = timeInMs - ( months * kMonth ); + result = "" + months + " months"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kWeek ) + { + final long weeks = timeInMs / kWeek; + final long remaining = timeInMs - ( weeks * kWeek ); + result = "" + weeks + " wks"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kDay ) + { + final long days = timeInMs / kDay; + final long remaining = timeInMs - ( days * kDay ); + result = "" + days + " days"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kHour ) + { + final long hrs = timeInMs / kHour; + final long remaining = timeInMs - ( hrs * kHour ); + result = "" + hrs + ( hrs == 1 ? " hr" : " hrs" ); + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kMinute ) + { + final long mins = timeInMs / kMinute; + final long remaining = timeInMs - ( mins * kMinute ); + result = "" + mins + " m"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else if ( timeInMs > kSecond ) + { + final long seconds = timeInMs / kSecond; + final long remaining = timeInMs - ( seconds * kSecond ); + result = "" + seconds + " s"; + if ( remaining > smallestUnit ) + { + result += ", "; + result += timeValue ( remaining, TimeUnit.MILLISECONDS, smallestUnit ); + } + } + else + { + result = "" + timeInMs + " ms"; + } + return result; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/oneWayHasher.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/oneWayHasher.java new file mode 100644 index 0000000..7dd7cdf --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/oneWayHasher.java @@ -0,0 +1,106 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class oneWayHasher +{ + public static String digest ( String input ) + { + return hash ( input, "" ); + } + + /** + * return a 20 byte (160 bit) hash of the input string, using a salt, if provided. + * @param input + * @param moreSalt + * @return 20 bytes + */ + @Deprecated + public static byte[] hashToBytes ( String input, String moreSalt ) + { + try + { + final StringBuffer fullMsg = new StringBuffer (); + fullMsg.append ( kPart1 ); + fullMsg.append ( input ); + fullMsg.append ( kPart2 ); + if ( moreSalt != null ) + { + fullMsg.append ( moreSalt ); + } + + final MessageDigest md = MessageDigest.getInstance ( "SHA-1" ); + md.reset (); + md.update ( fullMsg.toString().getBytes () ); + return md.digest (); // 160 bits + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException ( "MessageDigest can't find SHA-1 implementation." ); + } + } + + public static String pbkdf2HashToString ( String input, String salt ) + { + final byte[] bytes = pbkdf2Hash ( input, salt ); + return rrConvertor.bytesToHexString ( bytes ); + } + + public static byte[] pbkdf2Hash ( String input, String salt ) + { + try + { + final String algorithm = "PBKDF2WithHmacSHA1"; + final int derivedKeyLength = 160; + final int iterations = 20000; + + final KeySpec spec = new PBEKeySpec(input.toCharArray(), salt.getBytes (), iterations, derivedKeyLength); + final SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm); + return f.generateSecret(spec).getEncoded(); + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException ( e ); + } + catch ( InvalidKeySpecException e ) + { + throw new RuntimeException ( e ); + } + } + + @Deprecated + public static String hash ( String input, String moreSalt ) + { + final byte[] outBytes = hashToBytes ( input, moreSalt ); + return rrConvertor.bytesToHexString ( outBytes ); + } + + static public void main ( String args[] ) + { + if ( args.length != 1 && args.length != 2 ) + { + System.err.println ( "usage: oneWayHasher []" ); + } + else if ( args.length == 1 ) + { + System.out.println ( hash ( args[0], "" ) ); + } + else if ( args.length == 2 ) + { + System.out.println ( hash ( args[0], args[1] ) ); + } + } + + static private final String kPart1 = "In the fields of Rathravane, yell '"; + static private final String kPart2 = "!!!', and you'll find a rock thrown by a giant. "; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrConvertor.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrConvertor.java new file mode 100644 index 0000000..4fb3eb5 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrConvertor.java @@ -0,0 +1,606 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +import com.att.nsa.drumlin.till.data.base64.rrcBase64InputStream; +import com.att.nsa.drumlin.till.data.base64.rrcBase64OutputStream; + +public class rrConvertor +{ + public static class conversionError extends Exception + { + public conversionError ( String msg ) { super ( msg ); } + private static final long serialVersionUID = 1L; + } + + /** + * equivalent to Boolean.parseBoolean(s) + * @param s + * @return true/false + */ + static public boolean convertToBoolean ( String s ) + { + return Boolean.parseBoolean(s); + } + + /** + * a broader conversion of common strings for boolean values (e.g. yes/no, + * true/false, on/off). A null argument produces false. + * @param s + * @return true/false + */ + static public boolean convertToBooleanBroad ( String s ) + { + boolean result = + ( + s != null && ( + convertToBoolean ( s ) || + s.equalsIgnoreCase("true") || + s.equalsIgnoreCase("yes") || + s.equalsIgnoreCase("on") || + s.equalsIgnoreCase("1") || + s.equalsIgnoreCase("y") || + s.equalsIgnoreCase("checked") + ) + ); + return result; + } + + static public boolean convertToBoolean ( int i ) + { + return ( i != 0 ); + } + + static public boolean convertToBoolean ( long i ) + { + return ( i != 0L ); + } + + static public int convertToInt ( String s ) throws conversionError + { + int result = 0; + if ( s != null ) + { + try + { + result = Integer.parseInt ( s ); + } + catch ( NumberFormatException e ) + { + throw new conversionError ( "couldn't convert '" + s + "' to an integer" ); + } + } + return result; + } + + static public int convertToInt ( String s, int errval ) + { + try + { + return convertToInt ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + static public long convertToLong ( String s ) throws conversionError + { + long result = 0; + if ( s != null ) + { + try + { + result = Long.parseLong ( s ); + } + catch ( NumberFormatException e ) + { + throw new conversionError ( "couldn't convert " + s + " to a long" ); + } + } + return result; + } + + static public long convertToLong ( String s, long errval ) + { + try + { + return convertToLong ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + static public short convertToShort ( String s ) throws conversionError + { + short result = 0; + if ( s != null ) + { + try + { + result = Short.parseShort ( s ); + } + catch ( NumberFormatException e ) + { + throw new conversionError ( "couldn't convert " + s + " to a short" ); + } + } + return result; + } + + static public long convertToShort ( String s, long errval ) + { + try + { + return convertToShort ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + public static double convertToDouble ( String s ) throws conversionError + { + double result = 0.0; + if ( s != null ) + { + try + { + result = Double.parseDouble ( s ); + } + catch ( NumberFormatException e ) + { + throw new conversionError ( "couldn't convert " + s + " to a double" ); + } + } + return result; + } + + static public double convertToDouble ( String s, double errval ) + { + try + { + return convertToDouble ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + public static float convertToFloat ( String s ) throws conversionError + { + float result = 0.0f; + if ( s != null ) + { + try + { + result = Float.parseFloat ( s ); + } + catch ( NumberFormatException e ) + { + throw new conversionError ( "couldn't convert " + s + " to a double" ); + } + } + return result; + } + + static public float convertToFloat ( String s, float errval ) + { + try + { + return convertToFloat ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + public static char convertToCharacter ( String s ) throws conversionError + { + if ( s == null || s.length () != 1 ) + { + throw new conversionError ( "Expected a string with length 1." ); + } + return s.charAt ( 0 ); + } + + static public float convertToCharacter ( String s, char errval ) + { + try + { + return convertToCharacter ( s ); + } + catch ( conversionError e ) + { + return errval; + } + } + + public static byte[] convert ( int[] bytes ) throws conversionError + { + int index=0; + byte[] result = new byte [ bytes.length ]; + for ( int i : bytes ) + { + if ( i < 0 || i > 255 ) + { + throw new conversionError ( "In conversion to byte[], int[] contains value " + i ); + } + result[index++] = (byte) i; + } + return result; + } + + public static int[] convert ( byte[] bytes ) + { + int index=0; + int[] result = new int [ bytes.length ]; + for ( byte b : bytes ) + { + if ( b < 0 ) + { + result[index++] = (int) b + 256; + } + else + { + result[index++] = (int) b; + } + } + return result; + } + + public static String convertToString ( int[] bytes, int offset, int length ) throws conversionError + { + StringBuffer sb = new StringBuffer (); + for ( int i=offset; i 255 ) + { + throw new conversionError ( "Byte array encoded as int[] contains value " + c + ", which is out of range." ); + } + + int topNibble = c >>> 4; + int bottomNibble = c & 0x0f; + + sb.append ( nibbleToChar ( topNibble ) ); + sb.append ( nibbleToChar ( bottomNibble ) ); + } + return sb.toString (); + } + + public static char nibbleToChar ( int c ) throws conversionError + { + if ( c < 0 || c > 15 ) + { + throw new conversionError ( "Value " + c + " is not a valid nibble value." ); + } + if ( c < 10 ) + { + return (char)((int)'0' + c); + } + else + { + return (char)((int)'A' + c - 10); + } + } + + public static int charToNibble ( char c ) throws conversionError + { + char d = Character.toLowerCase ( c ); + if ( d >= '0' && d <= '9' ) + { + return ((int)d) - '0'; + } + else if ( d >= 'a' && d <= 'f' ) + { + return ((int)d) - 'a' + 10; + } + else + { + throw new conversionError ( "Character [" + c + "] is not a valid nibble." ); + } + } + public static int[] convertToByteArray ( String s ) throws conversionError + { + int len = s.length (); + if ( len % 2 != 0 ) + { + throw new conversionError ( "When converting to byte[], input string must be even length." ); + } + + int[] buffer = new int [ len / 2 ]; + int index = 0; + for ( int i=0; i> 4 ) ); + sb.append ( nibbleToHex ( b & 0x0f ) ); + return sb.toString (); + } + + public static String byteToHex ( int i ) + { + return byteToHex ( (byte)( i & 0xff )); + } + + public static String bytesToHex ( byte[] bb ) + { + return bytesToHex ( bb, 0, bb.length ); + } + + public static String bytesToHex ( byte[] bb, int offset, int length ) + { + StringBuffer sb = new StringBuffer (); + final int total = offset + length; + for ( int i=offset; i> 4 ) ); + sb.append ( nibbleToHex ( b & 0x0f ) ); + } + return sb.toString (); + } + + public static String stringToHex ( String s ) + { + return bytesToHex ( s.getBytes () ); + } + + public static String hexBytesToString ( String s ) throws conversionError + { + final byte[] bytes = hexToBytes ( s ); + return new String ( bytes ); + } + + public static String urlEncode ( String s ) + { + if ( s == null ) return null; + + try + { + return URLEncoder.encode ( s, "UTF-8" ); + } + catch ( UnsupportedEncodingException e ) + { + throw new RuntimeException ( e ); + } + } + + public static String urlDecode ( String s ) + { + if ( s == null ) return null; + + try + { + return URLDecoder.decode ( s, "UTF-8" ); + } + catch ( UnsupportedEncodingException e ) + { + throw new RuntimeException ( e ); + } + } + + private static char nibbleToHex ( int c ) + { + int result = c + '0'; + if ( c > 9 ) + { + result = c - 10 + 'A'; + } + return (char) result; + } + + /** + * encode the source string so that any occurrence of 'special' is duplicated + * @param source + * @param special + * @return an encoded string + */ + public static String encode ( String source, char special ) + { + return encode ( source, special, new char[]{}, new char[]{} ); + } + + /** + * encode the source string so that illegal chars are replaced by the replacement chars, + * with an escape sequence started by 'special'. If 'special' occurs, it'll occur twice + * in the encoded string. + * + * @param source + * @param special + * @param illegals + * @param replacements + * @return an encoded string + */ + public static String encode ( String source, char special, char[] illegals, char[] replacements ) + { + final String illStr = new String ( illegals ); + + final StringBuffer sb = new StringBuffer (); + for ( char c : source.toCharArray () ) + { + if ( c == special ) + { + sb.append ( special ); + sb.append ( special ); + } + else + { + int pos = illStr.indexOf ( c ); + if ( pos == -1 ) + { + sb.append ( c ); + } + else + { + sb.append ( special ); + sb.append ( replacements [ pos ] ); + } + } + } + return sb.toString (); + } + + public static String decode ( String encoded, char special ) + { + return decode ( encoded, special, new char[]{}, new char[]{} ); + } + + public static String decode ( String encoded, char special, char[] illegals, char[] replacements ) + { + final String repStr = new String ( replacements ); + + final StringBuffer sb = new StringBuffer (); + char chars[] = encoded.toCharArray (); + for ( int i=0; i 0 ) + { + final String part = key.substring ( 0, 2 ); + key = key.substring ( 2 ); + + final int partValue = Integer.parseInt ( part , 16 ); + baos.write ( partValue ); + } + return baos.toByteArray (); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrStreamTools.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrStreamTools.java new file mode 100644 index 0000000..089a2f6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/rrStreamTools.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * + * @author peter + * @Deprecated use StreamTools in saToolkit - same stuff, just moved + */ +@Deprecated +public class rrStreamTools +{ + protected static final int kBufferLength = 4096; + + /** + * Reads the stream into a byte array using a default-sized array for each read, + * then closes the input stream. + * + * @param is + * @param bufferSize + * @return a byte array + * @throws IOException + */ + public static byte[] readBytes ( InputStream is ) throws IOException + { + return readBytes ( is, kBufferLength ); + } + + /** + * Reads the stream into a byte array using a bufferSize array for each read, + * then closes the input stream. + * + * @param is + * @param bufferSize + * @return a byte array + * @throws IOException + */ + public static byte[] readBytes ( InputStream is, int bufferSize ) throws IOException + { + return readBytes ( is, bufferSize, -1 ); + } + + /** + * Reads the stream into a byte array using a bufferSize array for each read, + * then closes the input stream. If limit >= 0, at most limit bytes are read.
+ * Note: even with a negative limit, 2GB is the limit. + * + * @param is + * @param bufferSize + * @param limit + * @return a byte array + * @throws IOException + */ + public static byte[] readBytes ( InputStream is, int bufferSize, int limit ) throws IOException + { + int counter = 0; + final int atMost = limit < 0 ? Integer.MAX_VALUE : Math.min ( limit, Integer.MAX_VALUE ); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream (); + if ( is != null ) + { + byte[] b = new byte [ bufferSize ]; + int len = 0; + do + { + len = is.read ( b ); + if ( -1 != len ) + { + final int readNow = Math.min ( len, atMost - counter ); + baos.write ( b, 0, readNow ); + counter += readNow; + } + } + while ( len != -1 && counter < atMost ); + is.close (); + } + + return baos.toByteArray (); + } + + /** + * Copy from the input stream to the output stream, then close the output stream. + * @param in + * @param out + * @throws IOException + */ + public static void copyStream ( InputStream in, OutputStream out ) throws IOException + { + copyStream ( in, out, kBufferLength ); + } + + /** + * Copy from the input stream to the output stream, then close the output stream. + * @param in + * @param out + * @param bufferSize + * @throws IOException + */ + public static void copyStream ( InputStream in, OutputStream out, int bufferSize ) throws IOException + { + final byte[] buffer = new byte [bufferSize]; + int len; + while ( ( len = in.read ( buffer ) ) != -1 ) + { + out.write ( buffer, 0, len ); + } + out.close (); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/sha1HmacSigner.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/sha1HmacSigner.java new file mode 100644 index 0000000..c206cd9 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/sha1HmacSigner.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class sha1HmacSigner +{ + private static final String kHmacSha1Algo = "HmacSHA1"; + + public static String sign ( String message, String key ) + { + try + { + final SecretKey secretKey = new SecretKeySpec ( key.getBytes (), kHmacSha1Algo ); + final Mac mac = Mac.getInstance ( kHmacSha1Algo ); + mac.init ( secretKey ); + final byte[] rawHmac = mac.doFinal ( message.getBytes () ); + return rrConvertor.base64Encode ( rawHmac ); + } + catch ( InvalidKeyException e ) + { + throw new RuntimeException ( e ); + } + catch ( NoSuchAlgorithmException e ) + { + throw new RuntimeException ( e ); + } + catch ( IllegalStateException e ) + { + throw new RuntimeException ( e ); + } + } + + static public void main ( String args[] ) + { + if ( args.length != 2 ) + { + System.err.println ( "usage: sha1HmacSigner " ); + } + else if ( args.length == 2 ) + { + System.out.println ( sign ( args[0], args[1] ) ); + } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/stringUtils.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/stringUtils.java new file mode 100644 index 0000000..57fc681 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/stringUtils.java @@ -0,0 +1,320 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.util.LinkedList; +import java.util.List; + +public class stringUtils +{ + public static String emptyIfNull ( String s ) + { + return s == null ? "" : s; + } + + /** + * Return s, up to the first occurrence of delim. If delim does not occur, s is returned in full. + * @param s + * @param delim + * @return + */ + public static String substringTo ( String s, char delim ) + { + final int found = s.indexOf ( delim ); + return ( found > -1 ) ? s.substring ( 0, found ) : s; + } + + public static int isOneOf ( char c, char[] set ) + { + int result = -1; + for ( result = 0; result= set.length ? -1 : result; + } + + public static int indexOfAnyOf ( String s, char[] chars ) + { + return indexOfAnyOf ( s, chars, 0 ); + } + + public static int indexOfAnyOf ( String s, char[] chars, int fromIndex ) + { + int result = -1; + for ( int i=fromIndex; i 1 ) + { + sb.append ( s.substring ( 1 ).toLowerCase () ); + } + return sb.toString (); + } + + public static String dequote ( String s ) + { + return dequote ( s, new char[] { '"', '\'' } ); + } + + public static String dequote ( String s, char[] quoteChars ) + { + String result = s; + if ( indexOfAnyOf ( s, quoteChars ) == 0 ) + { + final char leading = s.charAt ( 0 ); + if ( s.charAt ( s.length () - 1 ) == leading ) + { + result = s.substring ( 1, s.length () - 1 ); + } + } + return result; + } + + public static String[] splitList ( String s ) + { + final List resultList = splitListToList ( s ); + return resultList.toArray ( new String [ resultList.size () ] ); + } + + public static String[] splitList ( String s, char[] separators, char[] quoteChars ) + { + final List resultList = splitListToList ( s, separators, quoteChars ); + return resultList.toArray ( new String [ resultList.size () ] ); + } + + public static List splitListToList ( String s ) + { + return splitListToList ( s, new char[] { ',', ';' }, new char[] { '\'', '"' } ); + } + + public static List splitListToList ( String s, char[] separators, char[] quoteChars ) + { + final LinkedList resultList = new LinkedList (); + + String remains = s; + while ( remains.length () > 0 ) + { + valueInfo vi = getLeadingValue ( remains, quoteChars, separators ); + if ( vi != null ) + { + if ( vi.fValue == null ) + { + vi = new valueInfo ( "", vi.fNextFieldAt ); + } + + resultList.add ( vi.fValue.trim () ); + if ( vi.fNextFieldAt > -1 ) + { + remains = remains.substring ( vi.fNextFieldAt ).trim (); + } + else + { + remains = ""; + } + } + } + return resultList; + } + + public static class valueInfo + { + public valueInfo () + { + this ( null, -1 ); + } + public valueInfo ( String val, int next ) + { + fValue = val; + fNextFieldAt = next; + } + public final String fValue; + public final int fNextFieldAt; + } + + public static valueInfo getLeadingValue ( String from ) + { + return getLeadingValue ( from, '\"', ',' ); + } + + public static valueInfo getLeadingValue ( String from, char quoteChar, char delimChar ) + { + return getLeadingValue ( from, new char[] { quoteChar }, new char[] { delimChar } ); + } + + public static valueInfo getLeadingValue ( String from, char[] quoteChars, char[] delimChars ) + { + valueInfo vi = new valueInfo (); + if ( from.length () > 0 ) + { + char current = from.charAt ( 0 ); + final int quoteId = isOneOf ( current, quoteChars ); + boolean quoted = ( quoteId != -1 ); + + if ( quoted ) + { + final char quoteChar = quoteChars [ quoteId ]; + + // scan for close quote + int foundEnd = -1; + int quoteScanFrom = 1; + while ( -1 == foundEnd ) + { + int quote = from.indexOf ( quoteChar, quoteScanFrom ); + if ( quote == -1 ) + { + // improper format! + break; + } + else + { + // check if this is a double quote inside the string or + // if this quote terminates the field + if ( quote + 1 < from.length () + && from.charAt ( quote + 1 ) == quoteChar ) + { + quoteScanFrom = quote + 2; + } + else + { + foundEnd = quote; + } + } + } + if ( foundEnd > -1 ) + { + StringBuffer fixedUp = new StringBuffer (); + String val = from.substring ( 1, foundEnd ); + boolean lastWasQuote = false; + for ( int i = 0; i < val.length (); i++ ) + { + char c = val.charAt ( i ); + if ( c == quoteChar ) + { + if ( !lastWasQuote ) + { + fixedUp.append ( c ); + } + // else: drop it + lastWasQuote = !lastWasQuote; + } + else + { + fixedUp.append ( c ); + lastWasQuote = false; + } + } + + final int nextFieldAt = indexOfAnyOf ( from, delimChars, foundEnd + 1 ); + vi = new valueInfo ( fixedUp.toString (), nextFieldAt != -1 ? nextFieldAt+1 : nextFieldAt ); + } + } + else + { + // scan for delimiter + int delim = indexOfAnyOf ( from, delimChars ); + if ( delim == -1 ) + { + vi = new valueInfo ( from, -1 ); + } + else + { + if ( delim == 0 ) + { + vi = new valueInfo ( null, 1 ); + } + else + { + vi = new valueInfo ( from.substring ( 0, delim ), delim+1 ); + } + } + } + } + return vi; + } + + public static class fieldInfo + { + public fieldInfo ( String val, int startPos ) + { + fValue = val; + fStartsAt = startPos; + } + public String toString () + { + return fValue + " [" + fStartsAt + "]"; + } + public final String fValue; + public final int fStartsAt; + } + + public static List split ( String from, char quoteChar, char delimChar ) + { + final LinkedList result = new LinkedList (); + + String remains = from; + int pos = 0; + while ( remains.length () > 0 ) + { + final valueInfo vi = getLeadingValue ( remains, quoteChar, delimChar ); + + final fieldInfo fi = new fieldInfo ( vi.fValue, pos ); + result.add ( fi ); + + if ( vi.fNextFieldAt > -1 ) + { + pos += vi.fNextFieldAt; + remains = remains.substring ( vi.fNextFieldAt ); + } + else + { + remains = ""; + } + } + + return result; + } + + public interface charSelector + { + boolean select ( Character c ); + } + + public static int indexOf ( String s, charSelector cc ) + { + final int len = s.length (); + int current = 0; + while ( current < len ) + { + final Character currChar = s.charAt ( current ); + if ( cc.select ( currChar ) ) + { + return current; + } + current++; + } + + return -1; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/uniqueStringGenerator.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/uniqueStringGenerator.java new file mode 100644 index 0000000..6d58843 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/data/uniqueStringGenerator.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.data; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * create a string that's very unlikely to be guessed + * @author peter + */ +public class uniqueStringGenerator +{ + public static String create ( ) + { + final byte[] val = createValue ( 8 ); + return rrConvertor.bytesToHexString ( val ); + } + + public static String createKeyUsingAlphabet ( String alphabet ) + { + final int alphabetLength = alphabet.length (); + final byte[] bytes = createValue ( 8 ); + final StringBuffer sb = new StringBuffer (); + for ( byte b : bytes ) + { + final int letterIndex = Math.abs ( b ) % alphabetLength; + final char letter = alphabet.charAt ( letterIndex ); + sb.append ( letter ); + } + return sb.toString (); + } + + public static String createKeyUsingAlphabet ( String alphabet, int length ) + { + String result = createKeyUsingAlphabet ( alphabet ); + while ( result.length () < length ) + { + result += createKeyUsingAlphabet ( alphabet ); + } + return result.substring ( 0, length ); + } + + public static String createUrlKey () + { + return createKeyUsingAlphabet ( kUrlKeyAlphabet ); + } + + public static String createMsStyleKeyString ( ) + { + final String original = createKeyUsingAlphabet ( kLicenseKeyAlphabet ); + + final StringBuffer sb = new StringBuffer (); + int position = -1; + for ( int i=0; i 0 && position % 5 == 0 ) + { + sb.append ( " " ); + } + sb.append ( letter ); + } + return sb.toString (); + } + + private static final String kLicenseKeyAlphabet = "123456789BCDFGHJKLMNPQRTVWXYZ"; + private static final String kUrlKeyAlphabet = "0123456789ABCDFGHJKLMNPQRTVWXYZabcdefhigjklmnopqrstuvwxyz"; + + public static byte[] createValue ( int size ) + { + try + { + final SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + byte[] salt = new byte[ size ]; + random.nextBytes(salt); + return salt; + } + catch ( NoSuchAlgorithmException e ) + { + log.error ( e.getMessage () ); + throw new RuntimeException ( e ); + } + } + + private static final Logger log = LoggerFactory.getLogger ( uniqueStringGenerator.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseReadable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseReadable.java new file mode 100644 index 0000000..311bf32 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseReadable.java @@ -0,0 +1,231 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Map; +import java.util.Map.Entry; + +import com.att.nsa.drumlin.till.data.rrConvertor; +import com.att.nsa.drumlin.till.data.rrConvertor.conversionError; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.rrNvWriteable; + +public abstract class nvBaseReadable implements rrNvReadable +{ + public abstract boolean hasValueFor ( String key ); + + public abstract String getString ( String key ) throws missingReqdSetting; + + protected nvBaseReadable () + { + } + + @Override + public String getString ( String key, String defValue ) + { + try + { + return getString ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public boolean getBoolean ( String key ) throws missingReqdSetting + { + return rrConvertor.convertToBoolean ( getString ( key ) ); + } + + @Override + public boolean getBoolean ( String key, boolean defValue ) + { + try + { + return getBoolean ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public int getInt ( String key ) throws missingReqdSetting + { + try + { + return rrConvertor.convertToInt ( getString ( key ) ); + } + catch ( conversionError e ) + { + throw new missingReqdSetting ( key, e ); + } + } + + @Override + public int getInt ( String key, int defValue ) + { + try + { + return getInt ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public double getDouble ( String key ) throws missingReqdSetting + { + try + { + return rrConvertor.convertToDouble ( getString ( key ) ); + } + catch ( conversionError e ) + { + throw new missingReqdSetting ( key, e ); + } + } + + @Override + public double getDouble ( String key, double defValue ) + { + try + { + return getDouble ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + public String[] getStrings ( String key ) throws missingReqdSetting + { + final String fullset = getString ( key ); + return fullset.split ( ",", -1 ); + } + + public String[] getStrings ( String key, String[] defValue ) + { + try + { + return getStrings ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public char getCharacter ( String key ) throws missingReqdSetting + { + try + { + return rrConvertor.convertToCharacter ( getString ( key ) ); + } + catch ( conversionError e ) + { + throw new missingReqdSetting ( key, e ); + } + } + + @Override + public char getCharacter ( String key, char defValue ) + { + try + { + return getCharacter ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public long getLong ( String key ) throws missingReqdSetting + { + try + { + return rrConvertor.convertToLong ( getString ( key ) ); + } + catch ( conversionError e ) + { + throw new missingReqdSetting ( key, e ); + } + } + + @Override + public long getLong ( String key, long defValue ) + { + try + { + return getLong ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + } + + @Override + public byte[] getBytes ( String key ) throws missingReqdSetting, invalidSettingValue + { + try + { + return rrConvertor.hexToBytes ( getString ( key ) ); + } + catch ( conversionError e ) + { + throw new invalidSettingValue ( key, e ); + } + } + + @Override + public byte[] getBytes ( String key, byte[] defValue ) + { + try + { + return getBytes ( key ); + } + catch ( missingReqdSetting e ) + { + return defValue; + } + catch ( invalidSettingValue e ) + { + return defValue; + } + } + + @Override + public void copyInto ( rrNvWriteable writeable ) + { + for ( Entry e : getCopyAsMap ().entrySet () ) + { + writeable.set ( e.getKey(), e.getValue () ); + } + } + + @Override + public void copyInto ( Map writeable ) + { + for ( Entry e : getCopyAsMap ().entrySet () ) + { + writeable.put ( e.getKey(), e.getValue () ); + } + } + + @Override + public void rescan () throws loadException + { + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseWriteable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseWriteable.java new file mode 100644 index 0000000..990e81a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvBaseWriteable.java @@ -0,0 +1,67 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import com.att.nsa.drumlin.till.data.rrConvertor; +import com.att.nsa.drumlin.till.nv.rrNvWriteable; + +public abstract class nvBaseWriteable extends nvBaseReadable implements rrNvWriteable +{ + @Override + public void set ( String key, char value ) + { + set ( key, "" + value ); + } + + @Override + public void set ( String key, boolean value ) + { + set ( key, new Boolean ( value ).toString () ); + } + + @Override + public void set ( String key, int value ) + { + set ( key, new Integer ( value ).toString () ); + } + + @Override + public void set ( String key, long value ) + { + set ( key, new Long ( value ).toString () ); + } + + @Override + public void set ( String key, double value ) + { + set ( key, new Double ( value ).toString () ); + } + + @Override + public void set ( String key, byte[] value ) + { + set ( key, rrConvertor.bytesToHex ( value ) ); + } + + @Override + public void set ( String key, byte[] value, int offset, int length ) + { + set ( key, rrConvertor.bytesToHex ( value, offset, length ) ); + } + + @Override + public void set ( String key, String[] values ) + { + final StringBuffer sb = new StringBuffer (); + boolean some = false; + for ( String val : values ) + { + if ( some ) sb.append ( "," ); + sb.append ( val ); + some = true; + } + set ( key, sb.toString () ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapper.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapper.java new file mode 100644 index 0000000..a411392 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapper.java @@ -0,0 +1,140 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeSet; + +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * This class acts as a wrapper around the basic rrNvReadable settings class, and it + * provides the ability to make settings specific to an "installation type" (e.g. + * debug, test, production). + * + * @author peter + * + */ +public class nvInstallTypeWrapper extends nvBaseReadable implements rrNvReadable +{ + public nvInstallTypeWrapper ( rrNvReadable actual ) + { + fActual = actual; + fKeys = new TreeSet (); + + fThisUser = System.getProperty ( "user.name" ); + + fSystemType = System.getProperty ( "sa.installation", null ); + if ( fSystemType != null ) + { + LoggerFactory.getLogger ( nvInstallTypeWrapper.class ).info ( "sa.installation: " + fSystemType ); + } + + parseForKeys (); + } + + @Override + public int size () + { + return fKeys.size (); + } + + @Override + public Collection getAllKeys () + { + return fKeys; + } + + @Override + public Map getCopyAsMap () + { + final HashMap map = new HashMap (); + for ( String key : fKeys ) + { + map.put ( key, getString ( key, "" ) ); + } + return map; + } + + @Override + public boolean hasValueFor ( String key ) + { + return fKeys.contains ( key ); + } + + @Override + public String getString ( String key ) throws missingReqdSetting + { + String result = null; + + // try keys from most specific to least + + if ( fSystemType != null && fThisUser != null ) + { + final String keyToTry = ( key + "[" + fSystemType + "@" + fThisUser + "]" ); + result = fActual.getString ( keyToTry, null ); + } + + if ( result == null && fSystemType != null ) + { + final String keyToTry = ( key + "[" + fSystemType + "]" ); + result = fActual.getString ( keyToTry, null ); + } + + if ( result == null && fThisUser != null ) + { + final String keyToTry = ( key + "[@" + fThisUser + "]" ); + result = fActual.getString ( keyToTry, null ); + } + + if ( result == null ) + { + result = fActual.getString ( key ); + } + + return result; + } + + @Override + public void rescan () throws loadException + { + super.rescan (); + parseForKeys (); + } + + private final rrNvReadable fActual; + private final TreeSet fKeys; + private final String fSystemType; + private final String fThisUser; + + private void parseForKeys () + { + fKeys.clear (); + for ( String key : fActual.getAllKeys () ) + { + fKeys.add ( parse ( key ) ); + } + } + + static String parse ( String key ) + { + // format: + // plain + // plain[sysType] + // plain[@user] + // plain[sysType@user] + key = key.trim (); + if ( key.matches ( ".*\\[[^\\[\\]]+\\]" ) ) + { + final int openBracket = key.indexOf ( '[' ); + key = key.substring ( 0, openBracket ); + } + return key; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvJvmSettings.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvJvmSettings.java new file mode 100644 index 0000000..928b502 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvJvmSettings.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeSet; + +public class nvJvmSettings extends nvBaseReadable +{ + public nvJvmSettings () + { + super (); + } + + public String getString ( String key ) throws missingReqdSetting + { + final String result = System.getProperty ( key ); + if ( result == null ) + { + throw new missingReqdSetting ( key ); + } + return result; + } + + @Override + public boolean hasValueFor ( String key ) + { + return System.getProperties ().containsKey ( key ); + } + + @Override + public int size () + { + return System.getProperties ().size (); + } + + @Override + public Collection getAllKeys () + { + final TreeSet list = new TreeSet (); + for ( Object o : System.getProperties ().keySet () ) + { + list.add ( o.toString () ); + } + return list; + } + + @Override + public Map getCopyAsMap () + { + HashMap map = new HashMap (); + for ( Entry e : System.getProperties ().entrySet () ) + { + map.put ( e.getKey().toString(), e.getValue().toString () ); + } + return map; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvPropertiesFile.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvPropertiesFile.java new file mode 100644 index 0000000..702b074 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvPropertiesFile.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.TreeSet; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public class nvPropertiesFile extends nvBaseReadable implements rrNvReadable +{ + public nvPropertiesFile ( File f ) throws loadException + { + super (); + + fFile = f; + fUrl = null; + fPrefs = new Properties (); + rescan (); + } + + public nvPropertiesFile ( URL u ) throws loadException + { + super (); + + fFile = null; + fUrl = u; + fPrefs = new Properties (); + rescan (); + } + + public String getString ( String key ) throws missingReqdSetting + { + final String result = fPrefs.getProperty ( key ); + if ( result == null ) + { + throw new missingReqdSetting ( key ); + } + return result; + } + + @Override + public boolean hasValueFor ( String key ) + { + return fPrefs.containsKey ( key ); + } + + @Override + public void rescan () throws loadException + { + try + { + fPrefs.clear (); + if ( fFile != null ) + { + read ( new FileInputStream ( fFile ) ); + } + else if ( fUrl != null ) + { + read ( fUrl.openStream () ); + } + else + { + log.warning ( "Rescanning a preferences table does not have a backing file or URL." ); + } + } + catch ( FileNotFoundException e ) + { + throw new loadException ( e ); + } + catch ( IOException e ) + { + throw new loadException ( e ); + } + } + + @Override + public int size () + { + return fPrefs.size (); + } + + @Override + public Collection getAllKeys () + { + final TreeSet list = new TreeSet (); + for ( Object o : fPrefs.keySet () ) + { + list.add ( o.toString () ); + } + return list; + } + + @Override + public Map getCopyAsMap () + { + HashMap map = new HashMap (); + for ( Entry e : fPrefs.entrySet () ) + { + map.put ( e.getKey().toString(), e.getValue().toString () ); + } + return map; + } + + private final File fFile; + private final URL fUrl; + private final Properties fPrefs; + + private static final Logger log = Logger.getLogger ( nvPropertiesFile.class.getName() ); + + private void read ( InputStream is ) throws loadException + { + try + { + fPrefs.load ( is ); + } + catch ( IOException e ) + { + throw new loadException ( e ); + } + } + + public String getFirstMatchingProperty(String key) throws missingReqdSetting { + for ( Object o : fPrefs.keySet () ) + { + //Change the asterisk to + if (Pattern.matches(o.toString().replace("*", "[^.]+"), key)) { + return fPrefs.getProperty(o.toString()); + } + } + + throw new missingReqdSetting ( key ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableStack.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableStack.java new file mode 100644 index 0000000..f951fe2 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableStack.java @@ -0,0 +1,122 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.TreeSet; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public class nvReadableStack extends nvBaseReadable implements rrNvReadable +{ + public nvReadableStack () + { + super (); + fStack = new LinkedList (); + } + + @Override + public String toString () + { + return getCopyAsMap().toString (); + } + + public void push ( rrNvReadable p ) + { + fStack.addFirst ( p ); + } + + public void pushBelow ( rrNvReadable below, rrNvReadable above ) + { + int i = fStack.indexOf ( above ); + if ( i < 0 ) + { + push ( below ); + } + else + { + fStack.add ( i+1, below ); + } + } + + public String getString ( String key ) throws missingReqdSetting + { + String result = null; + boolean found = false; + for ( rrNvReadable p : fStack ) + { + if ( p.hasValueFor ( key ) ) + { + result = p.getString ( key ); + found = true; + break; + } + } + + if ( !found ) + { + throw new missingReqdSetting ( key ); + } + + return result; + } + + public boolean hasValueFor ( String key ) + { + boolean result = false; + for ( rrNvReadable p : fStack ) + { + result = p.hasValueFor ( key ); + if ( result ) break; + } + return result; + } + + public void rescan () throws loadException + { + for ( rrNvReadable p : fStack ) + { + p.rescan (); + } + } + + private final LinkedList fStack; + + @Override + public int size () + { + return getAllKeys().size (); + } + + @Override + public Collection getAllKeys () + { + final TreeSet set = new TreeSet (); + for ( rrNvReadable r : fStack ) + { + set.addAll ( r.getAllKeys () ); + } + return set; + } + + @Override + public Map getCopyAsMap () + { + // this could be faster, but it's an easy way to get the correct values + final HashMap map = new HashMap (); + for ( String key : getAllKeys () ) + { + final String val = getString ( key, null ); + if ( val != null ) + { + map.put ( key, val ); + } + } + return map; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableTable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableTable.java new file mode 100644 index 0000000..d02312a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvReadableTable.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.TreeSet; + +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +public class nvReadableTable extends nvBaseReadable implements rrNvReadable +{ + public nvReadableTable () + { + this ( (Map)null ); + } + + public nvReadableTable ( Map content ) + { + if ( content != null ) + { + fTable = content; + } + else + { + fTable = new HashMap (); + } + } + + public nvReadableTable ( Properties content ) + { + fTable = new HashMap (); + for ( Entry e : content.entrySet () ) + { + fTable.put ( e.getKey().toString (), e.getValue ().toString () ); + } + } + + @Override + public String toString () + { + return fTable.toString (); + } + + public synchronized void clear ( String key ) + { + fTable.remove ( key ); + } + + public synchronized void clear () + { + fTable.clear (); + } + + public synchronized boolean hasValueFor ( String key ) + { + return fTable.containsKey ( key ); + } + + public synchronized String getString ( String key ) throws missingReqdSetting + { + final String result = fTable.get ( key ); + if ( result == null ) + { + throw new missingReqdSetting ( key ); + } + return result; + } + + @Override + public synchronized int size () + { + return fTable.size (); + } + + @Override + public synchronized Collection getAllKeys () + { + final TreeSet list = new TreeSet (); + for ( Object o : fTable.keySet () ) + { + list.add ( o.toString () ); + } + return list; + } + + @Override + public synchronized Map getCopyAsMap () + { + HashMap map = new HashMap (); + for ( Entry e : fTable.entrySet () ) + { + map.put ( e.getKey(), e.getValue() ); + } + return map; + } + + protected synchronized void set ( String key, String val ) + { + fTable.put ( key, val ); + } + + protected synchronized void set ( Map map ) + { + fTable.putAll ( map ); + } + + private final Map fTable; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvWriteableTable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvWriteableTable.java new file mode 100644 index 0000000..6a554ab --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/impl/nvWriteableTable.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv.impl; + +import java.util.Map; + +import com.att.nsa.drumlin.till.data.rrConvertor; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.rrNvWriteable; + +public class nvWriteableTable extends nvReadableTable implements rrNvWriteable +{ + public nvWriteableTable () + { + super (); + } + + public nvWriteableTable ( rrNvReadable that ) + { + super ( that == null ? null : that.getCopyAsMap () ); + if ( that != null ) + { + for ( String key : that.getAllKeys () ) + { + set ( key, that.getString ( key, null ) ); + } + } + } + + @Override + public synchronized void set ( String key, String value ) + { + super.set ( key, value ); + } + + @Override + public void set ( String key, char value ) + { + super.set ( key, "" + value ); + } + + public synchronized void set ( String key, int value ) + { + set ( key, "" + value ); + } + + public synchronized void set ( String key, long value ) + { + set ( key, "" + value ); + } + + public synchronized void set ( String key, double value ) + { + set ( key, "" + value ); + } + + public synchronized void set ( String key, boolean value ) + { + set ( key, "" + value ); + } + + public synchronized void set ( Map map ) + { + super.set ( map ); + } + + @Override + public synchronized void unset ( String key ) + { + super.clear ( key ); + } + + @Override + public synchronized void set ( String key, byte[] value ) + { + set ( key, value, 0, value.length ); + } + + @Override + public synchronized void set ( String key, byte[] value, int offset, int length ) + { + set ( key, rrConvertor.bytesToHex ( value, offset, length ) ); + } + + @Override + public void set ( String key, String[] values ) + { + final StringBuffer sb = new StringBuffer (); + boolean one = false; + for ( String value : values ) + { + if ( one ) sb.append ( "," ); + sb.append ( value ); + one = true; + } + set ( key, sb.toString () ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvReadable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvReadable.java new file mode 100644 index 0000000..9d07a22 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvReadable.java @@ -0,0 +1,80 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv; + +import java.util.Collection; +import java.util.Map; + +/** + * A data supplier + */ +public interface rrNvReadable +{ + class loadException extends Exception + { + public loadException ( Throwable cause ) { super(cause); } + private static final long serialVersionUID = 1L; + } + + class missingReqdSetting extends Exception + { + public missingReqdSetting ( String key ) { super("Missing required setting \"" + key + "\"" ); fKey = key; } + public missingReqdSetting ( String key, Throwable cause ) { super("Missing required setting \"" + key + "\" because " + cause.getMessage (), cause ); fKey=key; } + private static final long serialVersionUID = 1L; + public final String fKey; + } + + class invalidSettingValue extends Exception + { + public invalidSettingValue ( String key ) { super("Invalid setting for \"" + key + "\"" ); fKey=key; } + public invalidSettingValue ( String key, Throwable cause ) { super("Invalid setting for \"" + key + "\" because " + cause.getMessage (), cause ); fKey=key; } + public invalidSettingValue ( String key, String why ) { super("Invalid setting for \"" + key + "\" because " + why ); fKey=key; } + public invalidSettingValue ( String key, Throwable cause, String why ) { super("Invalid setting for \"" + key + "\" because " + why, cause ); fKey=key; } + private static final long serialVersionUID = 1L; + public final String fKey; + } + + String getString ( String key ) throws missingReqdSetting; + String getString ( String key, String defValue ); + + char getCharacter ( String key ) throws missingReqdSetting; + char getCharacter ( String key, char defValue ); + + boolean getBoolean ( String key ) throws missingReqdSetting; + boolean getBoolean ( String key, boolean defValue ); + + int getInt ( String key ) throws missingReqdSetting; + int getInt ( String key, int defValue ); + + long getLong ( String key ) throws missingReqdSetting; + long getLong ( String key, long defValue ); + + double getDouble ( String key ) throws missingReqdSetting; + double getDouble ( String key, double defValue ); + + byte[] getBytes ( String key ) throws missingReqdSetting, invalidSettingValue; + byte[] getBytes ( String key, byte[] defValue ); + + /** + * Get a set of strings given a key. Most implementations expect to use "getString()" and then + * split the value by commas. + * + * @param key + * @return a string array + * @throws missingReqdSetting + */ + String[] getStrings ( String key ) throws missingReqdSetting; + String[] getStrings ( String key, String[] defValue ); + + int size (); + boolean hasValueFor ( String key ); + Collection getAllKeys (); + Map getCopyAsMap (); + + void copyInto ( rrNvWriteable writeable ); + void copyInto ( Map writeable ); + + void rescan () throws loadException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvWriteable.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvWriteable.java new file mode 100644 index 0000000..f6e09e6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/nv/rrNvWriteable.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.nv; + +import java.util.Map; + +/** + * Write interface for a name/value pair container. + * + * @author peter@rathravane.com + */ +public interface rrNvWriteable extends rrNvReadable +{ + void clear (); + void unset ( String key ); + + void set ( String key, String value ); + void set ( String key, char value ); + void set ( String key, boolean value ); + void set ( String key, int value ); + void set ( String key, long value ); + void set ( String key, double value ); + void set ( String key, byte[] value ); + void set ( String key, byte[] value, int offset, int length ); + void set ( String key, String[] value ); + void set ( Map map ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrBlockFile.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrBlockFile.java new file mode 100644 index 0000000..5990538 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrBlockFile.java @@ -0,0 +1,653 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.store; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.charset.Charset; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; + +/** + * At the logical/interface level, this class reads and writes byte arrays to a + * file, assigning an address to each array. + *

+ * The implementation allocates fixed-size blocks in a random access file. Byte + * arrays are written to a chain of blocks. Each block has a block type flag and + * a 4 byte value that is either the address of the next block in this chain or, + * for the last block in the chain, the length of the data in the block. + * + * @author peter@rathravane.com + * + */ +public class rrBlockFile +{ + public static final long kBadHandle = -1; + + /** + * Initialize a block file with the given block size. If the file exists, + * its contents are destroyed. + * + * @param file + * @param blockSize + * @throws IOException + */ + public static void initialize ( File file, int blockSize ) throws IOException + { + // dumb as it would be, the minimum block size is the overhead + 1 byte + if ( blockSize < kOffsetToBlockData + 1 ) + { + throw new IllegalArgumentException ( "The block size is too small." ); + } + + final RandomAccessFile f = new RandomAccessFile ( file, "rw" ); + + // if the file exists, truncate it. This is required because new blocks are + // allocated with an index that is the length of the file. + f.setLength ( 0 ); + + // write the header, with block size and a random salt + f.seek ( 0 ); + f.write ( "rrbf".getBytes ( Charset.forName ( "UTF-8" ) )); + f.writeInt ( 1 ); + f.writeInt ( 0 ); + f.writeInt ( blockSize ); + f.writeLong ( kBadHandle ); + + byte[] salt = SecureRandom.getSeed ( kSaltSize ); + f.write ( salt ); + + // done + f.close (); + } + + /** + * Open an existing block file in read-write mode, and without a password. + * + * @param file + * @throws IOException + */ + public rrBlockFile ( File file ) throws IOException + { + this ( file, true ); + } + + /** + * Open an existing block file with the given read-write mode, and without + * a password. + * + * @param file + * @param withWrite + * @throws IOException + */ + public rrBlockFile ( File file, boolean withWrite ) throws IOException + { + this ( file, withWrite, null ); + } + + /** + * open an existing block file for read or read/write access + * @param file + * @param withWrite when true, open for writes + * @param passwd a password for the file, which can be null + * @throws FileNotFoundException + */ + public rrBlockFile ( File file, boolean withWrite, String passwd ) throws IOException + { + fUnderlyingFile = file; + fFile = new RandomAccessFile ( file, ( withWrite ? "rw" : "r" ) ); + fCanWrite = withWrite; + + fFile.seek ( 0 ); + + byte[] tag = new byte[4]; + fFile.read ( tag ); + String tagString = new String ( tag ); + if ( !tagString.equals ( "rrbf" ) ) + { + throw new IOException ( "unrecognized file format" ); + } + + fMajor = fFile.readInt (); + fMinor = fFile.readInt (); + if ( fMajor != 1 || fMinor != 0 ) + { + throw new IOException ( "unrecognized file format" ); + } + + fBlockSize = fFile.readInt (); + fCurrentBlockData = new byte [ fBlockSize ]; + fBlockDataSize = fBlockSize - kOffsetToBlockData; + + fDeleteChain = fFile.readLong (); + + if ( passwd != null ) + { + // read a salt from the file + byte[] saltBytes = new byte [ kSaltSize ]; + fFile.read ( saltBytes ); + initKey ( passwd, saltBytes ); + } + } + + public String getFilePath () + { + return fUnderlyingFile.getAbsolutePath (); + } + + /** + * Close the file. + * @throws IOException + */ + public void close () throws IOException + { + fFile.close (); + } + + /** + * Translate a 0-based block index to an address based on the block size used in this + * file. Note this is not generally useful, but applications that are careful can construct + * data in such as way as to place the data in known locations. For example, the first byte + * array (at "indexToAddress(0)") might contain a map to other important byte arrays. + * @param index + * @return an address value + */ + public long indexToAddress ( long index ) + { + return kHeaderLength + ( index * fBlockSize ); + } + + /** + * Add a byte array to this file and return its address. + * @param bytes + * @return the address for the stored byte array + * @throws IOException + */ + public long create ( byte[] bytes ) throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream ( bytes ); + return create ( bais ); + } + + /** + * Add a byte array from an input stream and return its address. + * @param is an input stream + * @return the address for the stored byte array + * @throws IOException + */ + public long create ( InputStream is ) throws IOException + { + long result = allocateBlock (); + final OutputStream os = writeStream ( result ); + copyStream ( is, os ); + os.close (); + return result; + } + + /** + * read a byte array from the file given its address + * @param address + * @return an array of bytes stored at the given address + * @throws IOException + */ + public byte[] read ( long address ) throws IOException + { + final InputStream in = readToStream ( address ); + final ByteArrayOutputStream baos = new ByteArrayOutputStream (); + copyStream ( in, baos ); + baos.close (); + return baos.toByteArray (); + } + + /** + * Read a stream to a byte array in the file given its address. + *

+ * NOTE: It's critical that no other methods on this object are called until + * you're finished reading the stream. + *

+ * @param address + * @return a stream to read + * @throws IOException + */ + public InputStream readToStream ( long address ) throws IOException + { + InputStream result = new blockReadStream ( address ); + if ( fKey != null ) + { + result = new CipherInputStream ( result, getCipher ( false ) ); + } + return result; + } + + /** + * Append the given byte array to the existing byte array at 'address'. + *

+ * Note that in password protected files, this operation can take some time, + * because the existing byte array must be read, decrypted, appended, and encrypted + * before being written back to the file. + *

+ * In clear files, the operation seeks to the end of the existing block + * chain and appends the new data. + * + * @param address + * @param bytes + * @throws IOException + */ + public void append ( long address, byte[] bytes ) throws IOException + { + if ( fKey != null ) + { + final byte[] thereNow = read ( address ); + final OutputStream os = writeStream ( address ); + os.write ( thereNow ); + os.write ( bytes ); + os.close (); + } + else + { + long lastBlock = getLastBlockInChain ( address ); + final byte[] thereNow = read ( lastBlock ); + final OutputStream os = writeStream ( lastBlock ); + os.write ( thereNow ); + os.write ( bytes ); + os.close (); + } + } + + /** + * Overwrite the existing byte array at 'address' with the given byte array. + * @param address + * @param bytes + * @throws IOException + */ + public void overwrite ( long address, byte[] bytes ) throws IOException + { + final ByteArrayInputStream bais = new ByteArrayInputStream ( bytes ); + overwrite ( address, bais ); + } + + /** + * Overwrite the existing byte array at 'address' with bytes from the given + * input stream. + * @param address + * @param bytes + * @throws IOException + */ + public void overwrite ( long address, InputStream bytes ) throws IOException + { + final OutputStream os = writeStream ( address ); + copyStream ( bytes, os ); + os.close (); + } + + /** + * Delete the byte array at 'address'. + * @param address + * @throws IOException + */ + public void delete ( long address ) throws IOException + { + if ( !fCanWrite ) + { + throw new IOException ( "opened read-only" ); + } + + // to delete a chain, we chain the current delete chain on to the back + // of this chain we're deleting, then set the front of the deleting + // chain in the header + + long current = getLastBlockInChain ( address ); + storeBlock ( current, fDeleteChain, new byte[0] ); + + fDeleteChain = address; + writeDeleteChainPointer (); + } + + private final File fUnderlyingFile; + private RandomAccessFile fFile; + private final boolean fCanWrite; + + // header + private final int fMajor; + private final int fMinor; + private final int fBlockSize; + private final int fBlockDataSize; + + private long fDeleteChain; + + private boolean fCurrentIsLast; + private long fCurrentNextOrSize; + private byte[] fCurrentBlockData; + + private PBEParameterSpec fParamSpec; + private SecretKey fKey; + + private static final int kSaltSize = 8; + private static final int kHeaderLength = + 4 + // "rrbf" + 4 + // major version + 4 + // minor version + 4 + // block size + 8 + // delete chain + kSaltSize // salt for password encrypted file + ; + private static final int kDeleteChainPointerLocation = 16; + private static final int kOffsetToBlockData = 8; // for size / next block pointer + + void copyStream ( InputStream is, OutputStream os ) throws IOException + { + copyStream ( is, os, fBlockSize ); + } + + static void copyStream ( InputStream is, OutputStream os, int bufferSize ) throws IOException + { + final byte[] buffer = new byte [ bufferSize ]; + int len; + while ( ( len = is.read ( buffer ) ) != -1 ) + { + os.write ( buffer, 0, len ); + } + } + + private long allocateBlock () throws IOException + { + long result = kBadHandle; + if ( fDeleteChain != kBadHandle ) + { + result = fDeleteChain; + fDeleteChain = getNextBlockFrom ( result ); + writeDeleteChainPointer (); + } + else + { + result = fFile.length (); + + // an earlier implementation left this out as an optimization. + // unfortunately, a long chain will allocate the next block + // before storing the current block. without the write here, the + // file length never changed, so the same block was reissued over + // and over. + storeBlock ( result, new byte[0], 0 ); + } + return result; + } + + private void writeDeleteChainPointer () throws IOException + { + fFile.seek ( kDeleteChainPointerLocation ); + fFile.writeLong ( fDeleteChain ); + } + + private void loadBlock ( long address ) throws IOException + { + long maxAddress = fFile.length (); + if ( address == maxAddress ) + { + fCurrentIsLast = true; + fCurrentNextOrSize = 0; + fCurrentBlockData = new byte [ 0 ]; + } + else + { + fFile.seek ( address ); + + int expect = fBlockDataSize; + final long sizeData = fFile.readLong (); + if ( sizeData >= 0 ) + { + fCurrentIsLast = true; + fCurrentNextOrSize = sizeData; + expect = (int) ( fCurrentNextOrSize & 0x0000ffff ); + } + else + { + fCurrentIsLast = false; + fCurrentNextOrSize = -1 * sizeData; + } + + fCurrentBlockData = new byte [ expect ]; + if ( expect > fFile.read ( fCurrentBlockData ) ) + { + throw new IOException ( "block size too small" ); + } + } + } + + private OutputStream writeStream ( long address ) throws IOException + { + OutputStream result = new blockOutputStream ( address ); + if ( fKey != null ) + { + result = new CipherOutputStream ( result, getCipher ( true ) ); + } + return result; + } + + private void storeBlock ( long thisBlock, long nextBlock, byte[] bytes ) throws IOException + { + fFile.seek ( thisBlock ); + + if ( nextBlock == kBadHandle ) + { + fFile.writeLong ( bytes.length ); + } + else + { + fFile.writeLong ( -1 * nextBlock ); + } + + final byte[] block = new byte [ fBlockDataSize ]; + System.arraycopy ( bytes, 0, block, 0, bytes.length ); + fFile.write ( block ); + } + + private void storeBlock ( long thisBlock, byte[] bytes, int size ) throws IOException + { + // storing the last block in a chain... + + if ( size < 0 ) + { + throw new IllegalArgumentException ( "Data size in last block may not be less than 0." ); + } + + long nextBlockWas = getNextBlockFrom ( thisBlock ); + + fFile.seek ( thisBlock ); + fFile.writeLong ( size ); + + final byte[] block = new byte [ fBlockDataSize ]; + System.arraycopy ( bytes, 0, block, 0, size ); + fFile.write ( block ); + + if ( nextBlockWas != kBadHandle ) + { + // the prior byte array continued into another block. that block + // is no longer needed, so delete it. (this is important in an + // overwrite case) + delete ( nextBlockWas ); + } + } + + private long getLastBlockInChain ( long handle ) throws IOException + { + long current = handle; + long next = getNextBlockFrom ( current ); + while ( next != kBadHandle ) + { + current = next; + next = getNextBlockFrom ( current ); + } + return current; + } + + private long getNextBlockFrom ( long handle ) throws IOException + { + long result = kBadHandle; + if ( handle != fFile.length () ) + { + fFile.seek ( handle ); + long nextOrSize = fFile.readLong (); + if ( nextOrSize < kBadHandle ) + { + result = nextOrSize * -1; + } + } + return result; + } + + // RFC 2898 recommends at least 1000 iterations... + private static final int kPbeIterationCount = 1000; + private static final int kPbeKeyLength = 8; + + private Cipher getCipher ( boolean toEncrypt ) throws IOException + { + if ( fKey == null ) + { + throw new IOException ( "Attempt to create cipher without key initialization." ); + } + try + { + final Cipher cipher = Cipher.getInstance ( fKey.getAlgorithm () ); + cipher.init ( ( toEncrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE ), fKey, fParamSpec ); + return cipher; + } + catch ( GeneralSecurityException e ) + { + throw new IOException ( e ); + } + } + + private void initKey ( String password, byte[] salt ) throws IOException + { + try + { + final PBEKeySpec keySpec = new PBEKeySpec ( password.toCharArray (), salt, kPbeIterationCount, kPbeKeyLength ); + fParamSpec = new PBEParameterSpec ( keySpec.getSalt (), keySpec.getIterationCount () ); + + final SecretKeyFactory keyFactory = SecretKeyFactory.getInstance ( "PBE" ); + fKey = keyFactory.generateSecret ( keySpec ); + } + catch ( NoSuchAlgorithmException e ) + { + throw new IOException ( e ); + } + catch ( InvalidKeySpecException e ) + { + throw new IOException ( e ); + } + } + + // NOTE: if the currently loaded block changes between calls to read(), + // this class will return unpredictable results. + private class blockReadStream extends InputStream + { + public blockReadStream ( long addr ) throws IOException + { + fCurrReadBlock = addr; + loadBlock ( fCurrReadBlock ); + fOffset = 0; + } + + @Override + public int read () throws IOException + { + if ( fCurrReadBlock == -1 ) + { + return -1; + } + + // make sure there's data available + final int dataInBlock = fCurrentBlockData.length; + if ( fOffset >= dataInBlock ) + { + // load next... + if ( !fCurrentIsLast ) + { + fCurrReadBlock = fCurrentNextOrSize; + loadBlock ( fCurrReadBlock ); + fOffset = 0; + } + else // last block + { + fCurrReadBlock = kBadHandle; + } + } + + int result = -1; + if ( fCurrReadBlock != kBadHandle ) + { + result = ( 0xff & fCurrentBlockData [ fOffset++ ] ); + } + return result; + } + + private long fCurrReadBlock; + private int fOffset; + } + + private class blockOutputStream extends OutputStream + { + public blockOutputStream ( long address ) throws IOException + { + if ( !fCanWrite ) + { + throw new IOException ( "opened read-only" ); + } + fCurrentBlock = address; + fBuffer = new byte [ fBlockDataSize ]; + fSize = 0; + } + + @Override + public void write ( int b ) throws IOException + { + if ( b > 127 || b < -128) + { + throw new IOException ( "byte value out of range" ); + } + + byte bb = (byte)( b & 0xff ); + if ( fSize < fBlockDataSize ) + { + fBuffer [ fSize++ ] = bb; + } + else + { + // buffer full, this byte goes in next block + long nextBlock = allocateBlock (); + storeBlock ( fCurrentBlock, nextBlock, fBuffer ); + fCurrentBlock = nextBlock; + fSize = 1; + fBuffer[0] = bb; + } + } + + @Override + public void close () throws IOException + { + storeBlock ( fCurrentBlock, fBuffer, fSize ); + } + + private long fCurrentBlock; + private byte[] fBuffer; + private int fSize; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrJsonObjectFile.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrJsonObjectFile.java new file mode 100644 index 0000000..6d941ac --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/store/rrJsonObjectFile.java @@ -0,0 +1,91 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.store; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; + +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +public class rrJsonObjectFile +{ + public static void initialize ( File file, int blockSize ) throws IOException + { + rrBlockFile.initialize ( file, blockSize ); + } + + public rrJsonObjectFile ( File f ) throws IOException + { + this ( f, true ); + } + + public rrJsonObjectFile ( File f, boolean withWrite ) throws IOException + { + this ( f, withWrite, null ); + } + + public rrJsonObjectFile ( File f, boolean withWrite, String passwd ) throws IOException + { + fFile = new rrBlockFile ( f, withWrite, passwd ); + } + + public String getFilePath () + { + return fFile.getFilePath (); + } + + public void close () throws IOException + { + fFile.close (); + } + + public JSONObject read ( long address ) throws IOException + { + JSONObject o = null; + final InputStream is = fFile.readToStream ( address ); + try + { + o = new JSONObject ( new JSONTokener ( new InputStreamReader ( is ) ) ); + } + catch ( JSONException e ) + { + throw new IOException ( e ); + } + finally + { + is.close (); + } + return o; + } + + public long write ( JSONObject o ) throws IOException + { + final byte[] b = o.toString ().getBytes ( Charset.forName ( "UTF-8" ) ); + return fFile.create ( b ); + } + + public void overwrite ( long address, JSONObject o ) throws IOException + { + final byte[] b = o.toString ().getBytes ( Charset.forName ( "UTF-8" ) ); + fFile.overwrite ( address, b ); + } + + public void delete ( long address ) throws IOException + { + fFile.delete ( address ); + } + + public long indexToAddress ( long index ) + { + return fFile.indexToAddress ( index ); + } + + private final rrBlockFile fFile; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/time/clock.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/time/clock.java new file mode 100644 index 0000000..3e8bd38 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/till/time/clock.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.till.time; + +import java.util.concurrent.TimeUnit; + +/** + * Basic clock service, replaces System.currentTimeMillis(), but with test access. + * + * @author peter + */ +public class clock +{ + public static long now () + { + return holder.instance.nowMs (); + } + + /** + * Provided for testing only. + * @param c + */ + public static void replaceClock ( clock c ) + { + holder.instance = c; + } + + /** + * Switch to a test clock and return that instance. Equivalent to instantiating + * a clock.testClock and calling replaceClock() with it. + * + * @return a test clock. + */ + public static testClock useNewTestClock () + { + final testClock tc = new testClock (); + replaceClock ( tc ); + return tc; + } + + protected long nowMs () + { + return System.currentTimeMillis (); + } + + private static class holder + { + // volatile: harmless in normal runs, as this is a singleton constructed + // once and shared among threads (all cache the same reference). For test + // runs (e.g. from JUnit), it ensures that replaceClock() takes effect in + // all threads immediately. + static volatile clock instance = new clock (); + } + + /** + * A simple testing clock. + * @author peter + */ + public static class testClock extends clock + { + @Override + public long nowMs () { return nowMs; } + + public void set ( long ms ) { nowMs = ms; } + public void add ( long ms ) { nowMs += ms; } + public void add ( long val, TimeUnit tu ) + { + add ( TimeUnit.MILLISECONDS.convert ( val, tu ) ); + } + + private long nowMs = 1; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/JsonBodyReader.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/JsonBodyReader.java new file mode 100644 index 0000000..d67768c --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/JsonBodyReader.java @@ -0,0 +1,294 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.LinkedList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * Read a JSON content body from a Drumlin request. + * @author peter@rathravane.com + * + */ +public class JsonBodyReader +{ + public static long kDefaultTimeoutMs = 1000 * 10; // 10 seconds + public static long kMaxBytes = 1024 * 1024 * 32; // 32MB + + /** + * Read the body of the request in the given context into a list of JSON objects. + * + * @param context + * @return a list of 0 or more JSONObjects + * @throws IOException + * @throws JSONException + */ + public static List readBodyForObjects ( DrumlinRequestContext context ) throws IOException, JSONException + { + return readBodyForObjects ( context, null ); + } + + /** + * read the request in the given context for a list of objects, optionally from the value + * named by 'path' in a top-level object. + * @param context + * @param path + * @return a list of 0 or more JSON objects + * @throws JSONException + */ + public static List readBodyForObjects ( DrumlinRequestContext context, String path ) throws JSONException + { + try + { + byte[] bytes = readBytes ( context.request (), kDefaultTimeoutMs ); + return readBodyForObjects ( bytes, path ); + } + catch ( IOException e ) + { + return new LinkedList (); + } + } + + /** + * read the bytes for objects. If the bytes contain a single JSON object and the path is + * not null, the objects are loaded from the value named by path rather than the top-level + * object. + * + * @param bytes + * @param path + * @return a list of 0 or more JSON objects + * @throws IOException + * @throws JSONException + */ + public static List readBodyForObjects ( final byte[] bytes, String path ) throws IOException, JSONException + { + final LinkedList result = new LinkedList (); + + // determine the first token in the stream to decide if we're reading a single + // object or an array. + boolean isSingleObject = false; + { + final ByteArrayInputStream s = new ByteArrayInputStream ( bytes ); + final JSONTokener t = new JSONTokener ( s ); + + char c = t.next (); + while ( Character.isWhitespace ( c ) ) c = t.next (); + + switch ( c ) + { + case '{': isSingleObject = true; break; + case '[': isSingleObject = false; break; + default: throw new JSONException ( "Expected an object or an array of objects." ); + } + s.close (); + } + + if ( isSingleObject ) + { + final String jsonStream = new String ( bytes, utf8 ); + final JSONObject o = new JSONObject ( jsonStream ); + + if ( path != null ) + { + final Object oo = o.opt ( path ); + if ( oo instanceof JSONObject ) + { + result.add ( (JSONObject) oo ); + } + else if ( oo instanceof JSONArray ) + { + result.addAll ( readArrayForObjects ( (JSONArray) oo ) ); + } + else + { + throw new JSONException ( "Couldn't read object at path [" + path + "]." ); + } + } + else + { + result.add ( o ); + } + } + else + { + final String jsonStream = new String ( bytes ); + final JSONArray a = new JSONArray ( jsonStream ); + result.addAll ( readArrayForObjects ( a ) ); + } + + return result; + } + + /** + * Read the request in the given context for a single JSON object, waiting at most the + * default timeout in milliseconds. + * @param context + * @return a JSONObject + * @throws IOException + * @throws JSONException + */ + public static JSONObject readBody ( DrumlinRequestContext context ) throws IOException, JSONException + { + return readBody ( context, kDefaultTimeoutMs ); + } + + /** + * Read the request in the given context for a single JSON object, waiting at most the + * given number of milliseconds. + * @param context + * @param timeoutMs + * @return a JSONObject + * @throws IOException + * @throws JSONException + */ + public static JSONObject readBody ( DrumlinRequestContext context, long timeoutMs ) throws IOException, JSONException + { + return readBody ( context.request (), timeoutMs ); + } + + /** + * Read the given request for a single JSON object, waiting at most the default + * number of milliseconds. + * @param req + * @return a JSONObject + * @throws IOException + * @throws JSONException + */ + public static JSONObject readBody ( DrumlinRequest req ) throws IOException, JSONException + { + return readBody ( req, kDefaultTimeoutMs ); + } + + /** + * Read the given request for a single JSON object, waiting at most the + * given number of milliseconds. + * @param req + * @param timeoutMs + * @return a JSONObject + * @throws IOException + * @throws JSONException + */ + public static JSONObject readBody ( DrumlinRequest req, long timeoutMs ) throws IOException, JSONException + { + byte[] bytes = readBytes ( req, timeoutMs ); + final String jsonStream = new String ( bytes ); + return new JSONObject ( jsonStream ); + } + + /** + * Read bytes from the given request, waiting at most timeout milliseconds + * @param req + * @param timeoutMs + * @return an array of bytes + * @throws IOException + */ + public static byte[] readBytes ( DrumlinRequest req, long timeoutMs ) throws IOException + { + final int clen = req.getContentLength (); + log.trace ( "Incoming content-length is " + clen ); + if ( clen == -1 ) + { + throw new IOException ( "The content length header must be provided." ); + } + else if ( clen > kMaxBytes ) + { + throw new IOException ( "Input too large." ); + } + + final ByteArrayOutputStream baos = new ByteArrayOutputStream (); + + final InputStream is = req.getBodyStream (); + if ( is == null ) + { + throw new IOException ( "No input stream on request." ); + } + + long totalBytesRead = 0; + try + { + byte[] b = new byte [ 4096 ]; + int len = 0; + long lastReadMs = System.currentTimeMillis (); + boolean complete = false; + + do + { + len = is.read ( b ); + if ( len > 0 ) + { + lastReadMs = System.currentTimeMillis (); + totalBytesRead += len; + if ( totalBytesRead > kMaxBytes ) + { + throw new IOException ( "Input too large." ); + } + baos.write ( b, 0, len ); + complete = ( totalBytesRead >= clen ); + } + else if ( len == 0 ) + { + if ( lastReadMs + timeoutMs < System.currentTimeMillis () ) + { + log.info ( "Read timed out. Total " + totalBytesRead + " bytes, content-length was " + clen ); + throw new IOException ( "Timed out waiting for input." ); + } + } + } + while ( len != -1 && !complete ); + } + finally + { + is.close (); + } + + if ( totalBytesRead < clen ) + { + throw new IOException ( "Expected " + clen + " bytes, received " + totalBytesRead ); + } + + // truncate input to content-length + final byte[] bytes = new byte [ clen ]; + System.arraycopy ( baos.toByteArray(), 0, bytes, 0, clen ); + return bytes; + } + + + private static List readArrayForObjects ( JSONArray a ) throws JSONException + { + final LinkedList result = new LinkedList (); + final int len = a.length (); + for ( int i=0; i objects ) throws IOException + { + final JSONArray out = new JSONArray ( objects ); + context.response (). + setStatus ( HttpStatusCodes.k200_ok ). + setContentType ( MimeTypes.kAppJson ). + send ( out.toString () + "\n" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/VeloJsonObject.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/VeloJsonObject.java new file mode 100644 index 0000000..15d5354 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/VeloJsonObject.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.util; + +import java.util.Collection; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VeloJsonObject +{ + public VeloJsonObject ( JSONObject o ) + { + fObject = o; + } + + public Object get ( String key ) + { + try + { + final Object o = fObject.get ( key ); + if ( o instanceof JSONObject ) + { + return new VeloJsonObject ( (JSONObject) o ); + } + else if ( o != null ) + { + return o.toString (); + } + } + catch ( JSONException e ) + { + log.info ( e.getMessage(), e ); + } + return null; + } + + public String getString ( String key, String defValue ) + { + return fObject.optString ( key, defValue ); + } + + public boolean hasValueFor ( String key ) + { + return fObject.has ( key ); + } + + @SuppressWarnings("unchecked") + public Collection getAllKeys () + { + return fObject.keySet (); + } + + private final JSONObject fObject; + private static final Logger log = LoggerFactory.getLogger ( VeloJsonObject.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/rrVeloLogBridge.java b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/rrVeloLogBridge.java new file mode 100644 index 0000000..13129e4 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/drumlin/util/rrVeloLogBridge.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.drumlin.util; + +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.log.LogChute; +import org.slf4j.Logger; + +public class rrVeloLogBridge implements org.apache.velocity.runtime.log.LogChute +{ + public rrVeloLogBridge ( Logger log ) + { + this.log = log; + } + + @Override + public void init ( RuntimeServices rs ) throws Exception + { + } + + @Override + public void log ( int level, String message ) + { + log ( level, message, null ); + } + + @Override + public void log ( int level, String message, Throwable t ) + { + switch ( level ) + { + case LogChute.DEBUG_ID: { if ( t == null ) log.debug ( message ); else log.debug ( message, t ); } break; + case LogChute.INFO_ID: { if ( t == null ) log.info ( message ); else log.info ( message, t ); } break; + case LogChute.WARN_ID: { if ( t == null ) log.warn ( message ); else log.warn ( message, t ); } break; + case LogChute.ERROR_ID: { if ( t == null ) log.error ( message ); else log.error ( message, t ); } break; + } + } + + @Override + public boolean isLevelEnabled ( int level ) + { + switch ( level ) + { + case LogChute.DEBUG_ID: { return log.isDebugEnabled (); } + case LogChute.INFO_ID: { return log.isInfoEnabled (); } + case LogChute.WARN_ID: { return log.isWarnEnabled (); } + case LogChute.ERROR_ID: { return log.isErrorEnabled (); } + } + return false; + } + + private final Logger log; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaAcl.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAcl.java new file mode 100644 index 0000000..d79a991 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAcl.java @@ -0,0 +1,153 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import java.util.Set; +import java.util.TreeSet; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An ACL record. When active, a user (API key) must have an explicit entries + * to be allowed access. + * @author peter + * + */ +public class NsaAcl +{ + /** + * Construct an ACL from a JSON string + * @param s + * @param nullReturnsEmpty + * @return an ACL + */ + public static NsaAcl fromJson ( String s, boolean nullReturnsEmpty ) + { + final JSONObject o = s == null ? null : new JSONObject ( s.length() == 0 ? "{}" : s ); + return fromJson ( o, nullReturnsEmpty ); + } + + /** + * Construct an ACL from a JSON object. If the JSON is null, + * null is returned. This indicates the lack of an ACL. + * @param o the object + * @param nullReturnsEmpty If true and the object is null, an empty ACL is returned. Otherwise, if the object is null, null is returned + * @return an ACL or null + */ + public static NsaAcl fromJson ( JSONObject o, boolean nullReturnsEmpty ) + { + if ( o == null && !nullReturnsEmpty ) return null; + + final NsaAcl acl = new NsaAcl (); + if ( o != null ) + { + final JSONArray a = o.optJSONArray ( "allowed" ); + if ( a != null ) + { + for ( int i=0; i (); + } + + /** + * Activate this ACL + */ + public void activate () + { + fActive = true; + } + + /** + * is this ACL active? + * @return + */ + public boolean isActive () + { + return fActive; + } + + /** + * Can the user access this resource? + * @param user + * @param perm + * @return true if the user is explicitly allowed + */ + public boolean canUser ( String user ) + { + return !fActive || fAllowed.contains ( user ); + } + + /** + * Add an entry to the end of the list. + * @param userApiKey + */ + public void add ( String userApiKey ) + { + fAllowed.add ( userApiKey ); + } + + /** + * Remove a user's access + * @param userApiKey + */ + public void remove ( String userApiKey ) + { + fAllowed.remove ( userApiKey ); + } + + /** + * Get the users on this ACL + * @return the user set + */ + public Set getUsers () + { + return new TreeSet ( fAllowed ); + } + + /** + * serialize to a json string + * @return a serialized JSON string + */ + public JSONObject serialize () + { + if ( !fActive ) + { + log.warn ( "Serializing an inactive ACL. (Inactive ACLs are deprecated.)" ); + } + + final JSONObject o = new JSONObject (); + o.put ( "active", fActive ); + final JSONArray a = new JSONArray (); + for ( String u : fAllowed ) + { + a.put ( u ); + } + o.put ( "allowed", a ); + return o; + } + + private boolean fActive; + private final TreeSet fAllowed; + + private static final Logger log = LoggerFactory.getLogger ( NsaAcl.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaAclUtils.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAclUtils.java new file mode 100644 index 0000000..f099f87 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAclUtils.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import java.util.Collection; +import java.util.LinkedList; + +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; + +public class NsaAclUtils +{ + public static void checkUserAccess ( String owner, NsaAcl acl, NsaApiKey user ) throws AccessDeniedException + { + final LinkedList owners = new LinkedList (); + owners.add ( owner ); + checkUserAccess ( owners, acl, user ); + } + + /** + * Throw an exception if the user is not authorized. If the ACL is null, + * all users are authorized. Otherwise, if the user is null, or the ACL + * does not allow the given user, an exception is thrown. + * @param owners + * @param acl + * @param user + * @throws AccessDeniedException + */ + public static void checkUserAccess ( Collection owners, NsaAcl acl, NsaApiKey user ) throws AccessDeniedException + { + // no acl = open + if ( acl == null ) return; + + // UEB's topic ownership records sometimes exist but with an empty (single) owner + // these are equivalent to empty ACLs + if ( owners.size () == 1 && owners.iterator ().next ().length () == 0 ) + { + return; + } + + // we have an acl. we must have a user that either matches the owner or is allowed by the ACL + if ( user == null ) + { + throw new AccessDeniedException ( "(no user)" ); + } + + // check the owner list + final String userKey = user.getKey (); + if ( owners.contains ( userKey ) ) + { + // user is an owner, continue + return; + } + + // we have an acl. we must have a user that either matches the owner or is allowed by the ACL + if ( !acl.canUser ( user.getKey () ) ) + { + throw new AccessDeniedException ( user.getKey() ); + } + } + + /** + * Update a resource's ACLs with the given API key either as a reader or writer, and either adding or removing access. + * @param resource the resource whose ACLs to change + * @param asUser the user making the change + * @param theApiKey the API key to add/remove + * @param forReadAccess true if this is for read access, false for write access + * @param add true if this is an add (grant), false if removing (revoking) + * @return the updated ACL (caller needs to know which it is based on forReadAccess) + * @throws AccessDeniedException if the user is not authorized to make this change + */ + public static NsaAcl updateAcl ( ReadWriteSecuredResource resource, NsaApiKey asUser, String theApiKey, boolean forReadAccess, boolean add ) throws AccessDeniedException + { + if ( !resource.getOwners().contains ( asUser.getKey() ) ) + { + throw new AccessDeniedException ( "User " + asUser.getKey() + " does not own topic " + resource.getName() ); + } + + NsaAcl acl = null; + if ( forReadAccess ) + { + acl = resource.getReaderAcl(); + } + else + { + acl = resource.getWriterAcl(); + } + + if ( acl == null ) + { + acl = new NsaAcl (); + } + + if ( add ) + { + acl.add ( theApiKey ); + } + else + { + acl.remove ( theApiKey ); + } + + // some older ACL writes incorrectly left the ACL as inactive. This system doesn't + // ever deactivate an ACL, but explicitly activate them all on writes. + acl.activate (); + + return acl; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaApiKey.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaApiKey.java new file mode 100644 index 0000000..6a4dd5d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaApiKey.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +/** + * An API key record. Note that any changes to the record via set() or enable/disable must + * be written back to the API key db explicitly. This class doesn't write to an underlying + * store. + * + * @author peter + */ +public interface NsaApiKey +{ + /** + * Get the unique API key value + * @return the API key + */ + String getKey (); + + /** + * Get the shared secret used for signing requests. + * @return the API secret for this key + */ + String getSecret (); + + /** + * Return true if this key is currently enabled. + * @return true if enabled + */ + boolean enabled (); + + /** + * Enable this key. + */ + void enable (); + + /** + * Disable this key. + */ + void disable (); + + /** + * Set additional data on the key record. For example, a username or email, + * or app-level capability information. + * @param key + * @param val + */ + void set ( String key, String val ); + + /** + * Get data from the key record other than the key and shared secret + * @param key + * @return a value, or null if none was set + */ + String get ( String key ); + + /** + * Get data from the key record other than the key and shared secret + * @param key + * @param defVal the value to use if none is present + * @return the value, or the default + */ + String get ( String key, String defVal ); + + /** + * serialize this key for the config db + * @return a string form of the key (typically JSON) + */ + String serialize (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticator.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticator.java new file mode 100644 index 0000000..6e37cbf --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticator.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; + +/** + * An interface for authenticating an inbound request. + * @author peter + */ +public interface NsaAuthenticator +{ + /** + * Qualify a request as possibly using the authentication method that this class implements. + * @param req + * @return true if the request might be authenticated by this class + */ + boolean qualify ( DrumlinRequest req ); + + /** + * Check for a request being authentic. If it is, return the API key. If not, return null. + * @param req An inbound web request + * @return the API key for an authentic request, or null + */ + K isAuthentic ( DrumlinRequest req ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticatorService.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticatorService.java new file mode 100644 index 0000000..e32dcc6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthenticatorService.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import java.util.LinkedList; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +/** + * A service for authenticating inbound requests. + * @author peter + * + * @param + */ +public class NsaAuthenticatorService +{ + /** + * Construct the security manager against an API key database with a specific request time window size + * @param requireSecureChannel if true, requests must be over a secure HTTPS channel + */ + public NsaAuthenticatorService ( boolean requireSecureChannel ) + { + fAuthenticators = new LinkedList> (); + fRequireSecureChannel = requireSecureChannel; + } + + /** + * Add an authenticator to this service. + * @param a + */ + public void addAuthenticator ( NsaAuthenticator a ) + { + fAuthenticators.add ( a ); + } + + /** + * Authenticate a user's request. This method returns the API key if the user is authentic, null otherwise. + * + * @param ctx + * @return an api key record, or null + */ + public K authenticate ( DrumlinRequestContext ctx ) + { + final DrumlinRequest req = ctx.request(); + + // NOTE: this is important... if a user attempts to authenticate over an insecure channel, + // the authentication request can potentially be spied on and replayed. Therefore, every + // authentication must come in over a secure channel. + if ( fRequireSecureChannel && !req.isSecure () ) + { + log.debug ( "Authentication request over insecure channel automatically fails." ); + return null; + } + + for ( NsaAuthenticator a : fAuthenticators ) + { + if ( a.qualify ( req ) ) + { + final K k = a.isAuthentic ( req ); + if ( k != null ) return k; + } + // else: this request doesn't look right to any authenticator + } + return null; + } + + // ultimately, this can go away and always be considered 'true', but we need + // some transition time on existing cambria systems + private final boolean fRequireSecureChannel; + private final LinkedList> fAuthenticators; + + private static final Logger log = LoggerFactory.getLogger ( NsaAuthenticatorService.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthorizationService.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthorizationService.java new file mode 100644 index 0000000..d0a8198 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaAuthorizationService.java @@ -0,0 +1,76 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.security.db.AuthorizationServiceUnavailableException; +import com.att.nsa.security.db.NsaAuthDb; + +/** + * A service for authorizing inbound requests + * @deprecated we don't want to authorize based on HTTP transaction + */ +@Deprecated +public class NsaAuthorizationService { + + private final NsaAuthDb authDb; + + public NsaAuthorizationService(NsaAuthDb authDb) { + this.authDb = authDb; + } + + public void permitAll(K key, DrumlinRequestContext ctx) throws AuthorizationServiceUnavailableException { + + if (key == null) throw new IllegalArgumentException("Key cannot be null"); + + final String resource = ctx.request().getUrl(); +// final String operation = ctx.request().getMethod(); + + getAuthDb().permitAll(resource); + } + + public void permit(K key, DrumlinRequestContext ctx) throws AuthorizationServiceUnavailableException { + + if (key == null) throw new IllegalArgumentException("Key cannot be null"); + + final String resource = ctx.request().getUrl(); +// final String operation = ctx.request().getMethod(); + + getAuthDb().permit(key, resource); + } + + public void denyAll(K key, DrumlinRequestContext ctx) throws AuthorizationServiceUnavailableException { + + if (key == null) throw new IllegalArgumentException("Key cannot be null"); + + final String resource = ctx.request().getUrl(); +// final String operation = ctx.request().getMethod(); + + getAuthDb().denyAll(resource); + } + + public void deny(K key, DrumlinRequestContext ctx) throws AuthorizationServiceUnavailableException { + + if (key == null) throw new IllegalArgumentException("Key cannot be null"); + + final String resource = ctx.request().getUrl(); +// final String operation = ctx.request().getMethod(); + + getAuthDb().deny(key, resource); + } + + public boolean isAuthorized(K key, DrumlinRequestContext ctx) throws AuthorizationServiceUnavailableException { + + if (key == null) throw new IllegalArgumentException("Key cannot be null"); + + final String resource = ctx.request().getUrl(); + final String operation = ctx.request().getMethod(); + + return getAuthDb().isAuthorized(key, resource, operation); + } + + private NsaAuthDb getAuthDb() { + return this.authDb; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/NsaSecurityManagerException.java b/saserverlibrary/src/main/java/com/att/nsa/security/NsaSecurityManagerException.java new file mode 100644 index 0000000..a641505 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/NsaSecurityManagerException.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +public class NsaSecurityManagerException extends Exception { + + public NsaSecurityManagerException(String msg) { super(msg); } + public NsaSecurityManagerException(Throwable t) { super(t); } + public NsaSecurityManagerException(String msg, Throwable t) { super(msg,t); } + private static final long serialVersionUID = 1L; + +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/ReadWriteSecuredResource.java b/saserverlibrary/src/main/java/com/att/nsa/security/ReadWriteSecuredResource.java new file mode 100644 index 0000000..831cbe6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/ReadWriteSecuredResource.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import java.util.Set; + +import com.att.nsa.configs.ConfigDbException; + +/** + * An interface for resources that have read and write ACLs. + * @author peter + * + */ +public interface ReadWriteSecuredResource +{ + /** + * Access denied exception + */ + public class AccessDeniedException extends Exception + { + public AccessDeniedException () { super ( "Access denied." ); } + public AccessDeniedException ( String user ) { super ( "Access denied for " + user ); } + private static final long serialVersionUID = 1L; + } + + /** + * a name for this resource + * @return a name + */ + String getName (); + + /** + * Get the set of owner API keys for this resource. Do not return null. + * @return a set of API keys + */ + Set getOwners (); + + /** + * Get the ACL for reading on this topic. Can be null. + * @return an ACL or null + */ + // FIXME: we may not want to dictate the use of separate reader/writer ACLs + NsaAcl getReaderAcl (); + + /** + * Get the ACL for writing on this topic. Can be null. + * @return an ACL or null + */ + // FIXME: we may not want to dictate the use of separate reader/writer ACLs + NsaAcl getWriterAcl (); + + /** + * Check if this user can read the topic. Throw otherwise. Note that + * user may be null. + * @param user + */ + void checkUserRead ( NsaApiKey user ) throws AccessDeniedException; + + /** + * Check if this user can write to the topic. Throw otherwise. Note + * that user may be null. + * @param user + */ + void checkUserWrite ( NsaApiKey user ) throws AccessDeniedException; + + /** + * allow the given user to publish + * @param apiKey + * @param asUser the user making this change, who must be authorized to do so + */ + void permitWritesFromUser ( String apiKey, NsaApiKey asUser ) throws AccessDeniedException, ConfigDbException; + + /** + * deny the given user from publishing + * @param apiKey + * @param asUser the user making this change, who must be authorized to do so + */ + void denyWritesFromUser ( String apiKey, NsaApiKey asUser ) throws AccessDeniedException, ConfigDbException; + + /** + * allow the given user to read the topic + * @param apiKey + * @param asUser the user making this change, who must be authorized to do so + */ + void permitReadsByUser ( String apiKey, NsaApiKey asUser ) throws AccessDeniedException, ConfigDbException; + + /** + * deny the given user from reading the topic + * @param apiKey + * @param asUser the user making this change, who must be authorized to do so + * @throws ConfigDbException + */ + void denyReadsByUser ( String apiKey, NsaApiKey asUser ) throws AccessDeniedException, ConfigDbException; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/MechIdAuthenticator.java b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/MechIdAuthenticator.java new file mode 100644 index 0000000..15e455a --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/MechIdAuthenticator.java @@ -0,0 +1,50 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.authenticators; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.NsaAuthenticator; +import com.att.nsa.security.db.NsaApiDb; + +/** + * An authenticator for AT&T MechIds. + * + * @author peter + * + * @param + */ +public class MechIdAuthenticator implements NsaAuthenticator +{ + public MechIdAuthenticator ( NsaApiDb db ) + { +// fDb = db; + } + + @Override + public boolean qualify ( DrumlinRequest req ) + { + // we haven't implemented anything here yet, so there's no qualifying request + return false; + } + + @Override + public K isAuthentic ( DrumlinRequest req ) + { + final String remoteAddr = req.getRemoteAddress (); + authLog ( "MechId auth is not yet implemented.", remoteAddr ); + return null; + } + + private static void authLog ( String msg, String remoteAddr ) + { + log.info ( "AUTH-LOG(" + remoteAddr + "): " + msg ); + } + +// private final NsaApiDb fDb; + private static final Logger log = LoggerFactory.getLogger ( MechIdAuthenticator.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/OriginalUebAuthenticator.java b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/OriginalUebAuthenticator.java new file mode 100644 index 0000000..2df9515 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/OriginalUebAuthenticator.java @@ -0,0 +1,234 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.authenticators; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.till.data.sha1HmacSigner; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.NsaAuthenticator; +import com.att.nsa.security.db.NsaApiDb; + +/** + * This authenticator handles an AWS-like authentication, originally used by the Cambria + * server (the API server for UEB). + * + * @author peter + * + * @param + */ +public class OriginalUebAuthenticator implements NsaAuthenticator +{ + public OriginalUebAuthenticator ( NsaApiDb db, long requestTimeWindowMs ) + { + fDb = db; + fRequestTimeWindowMs = requestTimeWindowMs; + } + + @Override + public boolean qualify ( DrumlinRequest req ) + { + // accept anything that comes in with X-(Cambria)Auth in the header + final String xAuth = getFirstHeader ( req, new String[]{ "X-CambriaAuth", "X-Auth" } ); + return xAuth != null; + } + + @Override + public K isAuthentic ( DrumlinRequest req ) + { + final String remoteAddr = req.getRemoteAddress (); + + // Cambria originally used "Cambria..." headers, but as the API key system is now more + // general, we take either form. + final String xAuth = getFirstHeader ( req, new String[]{ "X-CambriaAuth", "X-Auth" } ); + final String xDate = getFirstHeader ( req, new String[]{ "X-CambriaDate", "X-Date" } ); + + final String httpDate = req.getFirstHeader ( "Date" ); + + final String xNonce = getFirstHeader ( req, new String[]{ "X-Nonce" } ); + return authenticate ( remoteAddr, xAuth, xDate, httpDate, xNonce ); + } + + /** + * Authenticate a user's request. This method returns the API key if the user is authentic, null otherwise. + * + * @param ctx + * @return an api key record, or null + */ + public K authenticate ( String remoteAddr, String xAuth, String xDate, String httpDate, String nonce ) + { + if ( xAuth == null ) + { + authLog ( "No X-Auth header on request", remoteAddr ); + return null; + } + + final String[] xAuthParts = xAuth.split ( ":"); + if ( xAuthParts.length != 2 ) + { + authLog ( "Bad X-Auth header format (" + xAuth + ")", remoteAddr ); + return null; + } + + // get the api key and signature + final String clientApiKey = xAuthParts[0]; + final String clientApiHash = xAuthParts[1]; + if ( clientApiKey.length () == 0 || clientApiHash.length() == 0 ) + { + authLog ( "Bad X-Auth header format (" + xAuth + ")", remoteAddr ); + return null; + } + + // if the user provided X-Date, use that. Otherwise, go for Date + final String dateString = xDate != null ? xDate : httpDate; + final Date clientDate = getClientDate ( dateString ); + if ( clientDate == null ) + { + authLog ( "Couldn't parse client date '" + dateString + "'. Preferring X-Date over Date.", remoteAddr ); + return null; + } + + // check the time range + final long nowMs = System.currentTimeMillis (); + final long diffMs = Math.abs ( nowMs - clientDate.getTime () ); + if ( diffMs > fRequestTimeWindowMs ) + { + authLog ( "Client date is not in acceptable range of server date. Client:" + clientDate.getTime () + + ", Server: " + nowMs + ", Threshold: " + fRequestTimeWindowMs + ".", remoteAddr ); + return null; + } + + K apiRecord; + try + { + apiRecord = fDb.loadApiKey ( clientApiKey ); + if ( apiRecord == null ) + { + authLog ( "No API key " + clientApiKey + " in this authenticator.", remoteAddr ); + return null; + } + } + catch ( ConfigDbException e ) + { + authLog ( "Couldn't load API key " + clientApiKey + ": " + e.getMessage(), remoteAddr ); + return null; + } + + // make the signed content + final StringBuilder sb = new StringBuilder (); + sb.append ( dateString ); + if ( nonce != null ) + { + sb.append ( ":" ); + sb.append ( nonce ); + } + final String signedContent = sb.toString (); + + // now check the signed date string + final String serverCalculatedSignature = sha1HmacSigner.sign ( signedContent, apiRecord.getSecret () ); + if ( serverCalculatedSignature == null || !serverCalculatedSignature.equals ( clientApiHash ) ) + { + authLog ( "Signatures don't match. Rec'd " + clientApiHash + ", expect " + serverCalculatedSignature + ".", remoteAddr ); + return null; + } + + authLog ( "authenticated " + apiRecord.getKey (), remoteAddr ); + return apiRecord; + } + + /** + * Get the first value of the first existing header from the headers list + * @param req + * @param headers + * @return a header value, or null if none exist + */ + private static String getFirstHeader ( DrumlinRequest req, String[] headers ) + { + for ( String header : headers ) + { + final String result = req.getFirstHeader ( header ); + if ( result != null ) return result; + } + return null; + } + + /** + * Parse the date string into a Date using one of the supported date formats. + * @param dateHeader + * @return a date, or null + */ + static Date getClientDate ( String dateString ) + { + if ( dateString == null ) + { + return null; + } + + // parse the date + Date result = null; + for ( String dateFormat : kDateFormats ) + { + final SimpleDateFormat parser = new SimpleDateFormat ( dateFormat, java.util.Locale.US ); + if ( !dateFormat.contains ( "z" ) && !dateFormat.contains ( "Z" ) ) + { + parser.setTimeZone ( TIMEZONE_GMT ); + } + + try + { + result = parser.parse ( dateString ); + break; + } + catch ( ParseException e ) + { + // presumably wrong format + } + } + return result; + } + + private static void authLog ( String msg, String remoteAddr ) + { + log.info ( "AUTH-LOG(" + remoteAddr + "): " + msg ); + } + + private final NsaApiDb fDb; + private final long fRequestTimeWindowMs; + + private static final java.util.TimeZone TIMEZONE_GMT = java.util.TimeZone.getTimeZone("GMT"); + + private static final String kDateFormats[] = + { + // W3C date format (RFC 3339). + "yyyy-MM-dd'T'HH:mm:ssz", + "yyyy-MM-dd'T'HH:mm:ssXXX", // as of Java 7, reqd to handle colon in TZ offset + + // Preferred HTTP date format (RFC 1123). + "EEE, dd MMM yyyy HH:mm:ss zzz", + + // simple unix command line 'date' format + "EEE MMM dd HH:mm:ss z yyyy", + + // Common date format (RFC 822). + "EEE, dd MMM yy HH:mm:ss z", + "EEE, dd MMM yy HH:mm z", + "dd MMM yy HH:mm:ss z", + "dd MMM yy HH:mm z", + + // Obsoleted HTTP date format (ANSI C asctime() format). + "EEE MMM dd HH:mm:ss yyyy", + + // Obsoleted HTTP date format (RFC 1036). + "EEEE, dd-MMM-yy HH:mm:ss zzz", + }; + + private static final Logger log = LoggerFactory.getLogger ( OriginalUebAuthenticator.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/RemoteSaIamAuthenticator.java b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/RemoteSaIamAuthenticator.java new file mode 100644 index 0000000..c4843f6 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/RemoteSaIamAuthenticator.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.authenticators; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.apiClient.http.CacheUse; +import com.att.nsa.apiClient.http.HttpClient; +import com.att.nsa.apiClient.http.HttpClient.ConnectionType; +import com.att.nsa.apiClient.http.HttpException; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.drumlin.till.nv.rrNvReadable.missingReqdSetting; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.NsaAuthenticator; + +/** + * This authenticator handles contacts an authentication server for validation. + * + * @author peter + * + * @param + */ +public class RemoteSaIamAuthenticator implements NsaAuthenticator +{ + private static final int kStandardIamPort = 33333; + + private static final String kSetting_IamServers = "iam.remote.servers"; + + private static final String kSetting_IamCacheSize = "iam.remote.cacheSize"; + private static final int kDefault_IamCacheSize = 1024; + + private static final String kSetting_IamCacheMaxAgeSeconds = "iam.remote.cacheMaxAgeSeconds"; + private static final int kDefault_IamCacheMaxAgeSeconds = 60 * 15; + + private static final String kSetting_IamDisableCertCheck = "iam.remote.disableCertificateValidation"; + private static final boolean kDefault_IamDisableCertCheck = false; + + public interface ApiKeyFactory + { + K createApiKey ( JSONObject data ); + } + + public RemoteSaIamAuthenticator ( rrNvReadable settings, ApiKeyFactory factory ) throws missingReqdSetting, MalformedURLException, GeneralSecurityException + { + fSettings = settings; + fFactory = factory; + + final String[] iamServerList = fSettings.getStrings ( kSetting_IamServers ); + fServers = new LinkedList (); + fServers.addAll ( Arrays.asList ( iamServerList ) ); + + ConnectionType ct = ConnectionType.HTTPS; + if ( settings.getBoolean ( kSetting_IamDisableCertCheck, kDefault_IamDisableCertCheck ) ) + { + ct = ConnectionType.HTTPS_NO_VALIDATION; + log.warn ( "RemoteSaIamAuthenticator is running with HTTPS certificate validation disabled." ); + } + + fClient = new HttpClient ( ct, fServers, kStandardIamPort, + UUID.randomUUID ().toString (), // no session stickiness required, just load distribution + CacheUse.FULL, + fSettings.getInt ( kSetting_IamCacheSize, kDefault_IamCacheSize ), + fSettings.getInt ( kSetting_IamCacheMaxAgeSeconds, kDefault_IamCacheMaxAgeSeconds ), + TimeUnit.SECONDS + ); + } + + @Override + public boolean qualify ( DrumlinRequest req ) + { + // accept anything that comes in with X-(Cambria)Auth in the header + final String xAuth = getFirstHeader ( req, new String[]{ "X-CambriaAuth", "X-Auth" } ); + return xAuth != null; + } + + @Override + public K isAuthentic ( DrumlinRequest req ) + { + try + { + final String remoteAddr = req.getRemoteAddress (); + + // Cambria originally used "Cambria..." headers, but as the API key system is now more + // general, we take either form. + final String xAuth = getFirstHeader ( req, new String[]{ "X-CambriaAuth", "X-Auth" } ); + final String xDate = getFirstHeader ( req, new String[]{ "X-CambriaDate", "X-Date" } ); + + final String httpDate = req.getFirstHeader ( "Date" ); + + final String xNonce = getFirstHeader ( req, new String[]{ "X-Nonce" } ); + return authenticate ( remoteAddr, xAuth, xDate, httpDate, xNonce ); + } + catch ( GeneralSecurityException | IOException e ) + { + log.warn ( "Authentication service problem: " + e.getMessage (), e ); + return null; + } + } + + /** + * Authenticate a user's request. This method returns the API key if the user is authentic, null otherwise. + * + * @param ctx + * @return an api key record, or null + * @throws IOException + * @throws GeneralSecurityException + */ + public K authenticate ( String remoteAddr, String xAuth, String xDate, String httpDate, String nonce ) throws GeneralSecurityException, IOException + { + final JSONObject authData = new JSONObject (); + + if ( xAuth == null ) + { + authLog ( "No X-Auth header on request", remoteAddr ); + return null; + } + + final String[] xAuthParts = xAuth.split ( ":"); + if ( xAuthParts.length != 2 ) + { + authLog ( "Bad X-Auth header format (" + xAuth + ")", remoteAddr ); + return null; + } + + // get the api key and signature + authData.put ( "apiKey", xAuthParts[0] ); + authData.put ( "apiSignature", xAuthParts[1] ); + + if ( xAuthParts[0].length () == 0 || xAuthParts[1].length() == 0 ) + { + authLog ( "Bad X-Auth header format (" + xAuth + ")", remoteAddr ); + return null; + } + + // if the user provided X-Date, use that. Otherwise, go for Date + final String dateString = xDate != null ? xDate : httpDate; + authData.put ( "date", dateString ); + + if ( nonce != null ) + { + authData.put ( "nonce", nonce ); + } + + try + { + final JSONObject response = fClient.post ( "/v1/iam/authenticate", authData, true ); + final K key = fFactory.createApiKey ( response ); + authLog ( "Remote IAM authenticated " + key.getKey(), remoteAddr ); + return key; + } + catch ( HttpException e ) + { + authLog ( "Error with remote authentication: " + e.getMessage(), remoteAddr ); + return null; + } + } + + /** + * Get the first value of the first existing header from the headers list + * @param req + * @param headers + * @return a header value, or null if none exist + */ + private static String getFirstHeader ( DrumlinRequest req, String[] headers ) + { + for ( String header : headers ) + { + final String result = req.getFirstHeader ( header ); + if ( result != null ) return result; + } + return null; + } + + private static void authLog ( String msg, String remoteAddr ) + { + log.info ( "AUTH-LOG(" + remoteAddr + "): " + msg ); + } + + private final rrNvReadable fSettings; + private final ApiKeyFactory fFactory; + private final LinkedList fServers; + private final HttpClient fClient; + + private static final Logger log = LoggerFactory.getLogger ( RemoteSaIamAuthenticator.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/SimpleAuthenticator.java b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/SimpleAuthenticator.java new file mode 100644 index 0000000..b9f55e5 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/authenticators/SimpleAuthenticator.java @@ -0,0 +1,56 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.authenticators; + +import java.util.HashMap; + +import org.apache.commons.codec.binary.Base64; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequest; +import com.att.nsa.security.NsaAuthenticator; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKeyFactory; + +/** + * Authenticates an HTTP Basic auth request against explicitly added username/passwords. + * @author peter + * + */ +public class SimpleAuthenticator implements NsaAuthenticator +{ + public SimpleAuthenticator add ( String user, String password ) + { + fCreds.put ( user, password ); + return this; + } + + @Override + public boolean qualify ( DrumlinRequest req ) + { + final String auth = req.getFirstHeader ( "Authorization" ); + return auth != null && auth.startsWith ( "Basic " ); + } + + @Override + public NsaSimpleApiKey isAuthentic ( DrumlinRequest req ) + { + final String auth = req.getFirstHeader ( "Authorization" ).substring ( "Basic ".length () ); + final String decoded = new String ( Base64.decodeBase64 ( auth.getBytes () ) ); + final int colon = decoded.indexOf ( ":" ); + if ( colon > -1 ) + { + final String user = decoded.substring ( 0, colon ); + final String password = decoded.substring ( colon + 1 ); + final String lookup = fCreds.get ( user ); + if ( lookup != null && lookup.equals ( password ) ) + { + final NsaSimpleApiKeyFactory f = new NsaSimpleApiKeyFactory (); + return f.makeNewKey ( user, password ); + } + } + return null; + } + + private final HashMap fCreds = new HashMap (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/AuthorizationServiceUnavailableException.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/AuthorizationServiceUnavailableException.java new file mode 100644 index 0000000..c244c08 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/AuthorizationServiceUnavailableException.java @@ -0,0 +1,14 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +public class AuthorizationServiceUnavailableException extends Exception { + + private static final long serialVersionUID = 1L; + + public AuthorizationServiceUnavailableException() { super(); } + public AuthorizationServiceUnavailableException(String message) { super(message); } + public AuthorizationServiceUnavailableException(Throwable t) { super(t); }; + public AuthorizationServiceUnavailableException(String message, Throwable t) { super(message, t); } +} \ No newline at end of file diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/BaseNsaApiDbImpl.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/BaseNsaApiDbImpl.java new file mode 100644 index 0000000..1e39a8f --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/BaseNsaApiDbImpl.java @@ -0,0 +1,172 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import com.att.nsa.configs.ConfigDb; +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.ConfigPath; +import com.att.nsa.configs.JsonConfigDb; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; + +/** + * Persistent storage for API keys and secrets built over an abstract config db. + */ +public class BaseNsaApiDbImpl implements NsaApiDb +{ + /** + * Construct an API db over the given config db at the standard location + * @param db + * @throws ConfigDbException + */ + public BaseNsaApiDbImpl ( ConfigDb db, NsaApiKeyFactory keyFactory ) throws ConfigDbException + { + this ( db, kStdRootPath, keyFactory ); + } + + /** + * Construct an API db over the given config db using the given root location + * @param db + * @param rootPath + * @throws ConfigDbException + */ + public BaseNsaApiDbImpl ( ConfigDb db, String rootPath, NsaApiKeyFactory keyFactory ) throws ConfigDbException + { + fDb = new JsonConfigDb ( db ); + fBasePath = db.parse ( rootPath ); + fKeyFactory = keyFactory; + + if ( !db.exists ( fBasePath ) ) + { + db.store ( fBasePath, "" ); + } + } + + /** + * Load all keys known to this database. (This could be expensive.) + * @return a set of all API keys + * @throws ConfigDbException + */ + public synchronized Set loadAllKeys () throws ConfigDbException + { + final TreeSet result = new TreeSet (); + for ( ConfigPath cp : fDb.loadChildrenNames ( fBasePath ) ) + { + result.add ( cp.getName () ); + } + return result; + } + + /** + * Load all keys known to this database. (This could be expensive.) + * @return a map of api key to the api key record + * @throws ConfigDbException + */ + public synchronized Map loadAllKeyRecords () throws ConfigDbException + { + final HashMap result = new HashMap (); + + for ( Entry e : fDb.loadChildrenOf ( fBasePath ).entrySet () ) + { + final String val = e.getValue (); + if ( val != null ) + { + result.put ( e.getKey ().getName(), fKeyFactory.makeNewKey ( val ) ); + } + } + return result; + } + + /** + * Load an API key record based on the API key value + * @param apiKey + * @return an API key record or null + * @throws ConfigDbException + */ + public synchronized K loadApiKey ( String apiKey ) throws ConfigDbException + { + final String data = fDb.load ( makePath(apiKey) ); + if ( data != null ) + { + return fKeyFactory.makeNewKey ( data ); + } + return null; + } + + /** + * Save an API key record. This must be used after changing auxiliary data on the record. + * Note that the key must exist (via createApiKey). + * @param key + * @throws ConfigDbException + */ + public synchronized void saveApiKey ( K apiKey ) throws ConfigDbException + { + final ConfigPath path = makePath ( apiKey.getKey() ); + if ( !fDb.exists ( path ) || !(apiKey instanceof NsaSimpleApiKey) ) + { + throw new IllegalStateException ( apiKey.getKey() + " is not known to this database" ); + } + fDb.storeJson ( path, ((NsaSimpleApiKey)apiKey).serializeAsJson () ); + } + + /** + * Create a new API key. If one exists, + * @param key + * @param sharedSecret + * @return the new API key record + * @throws ConfigDbException + */ + public synchronized K createApiKey ( String key, String sharedSecret ) throws KeyExistsException, ConfigDbException + { + final ConfigPath path = makePath ( key ); + if ( fDb.exists ( path ) ) + { + throw new KeyExistsException ( key ); + } + + // make one, store it, return it + final K newKey = fKeyFactory.makeNewKey ( key, sharedSecret ); + fDb.store ( path, newKey.serialize () ); + return newKey; + } + + /** + * Delete an API key; equivalent to deleteApiKey ( key.getKey() ) + * @param key + * @return true if the key existed + * @throws ConfigDbException + */ + public synchronized boolean deleteApiKey ( K key ) throws ConfigDbException + { + return deleteApiKey ( key.getKey () ); + } + + /** + * Delete an API key from storage. + * @param key + * @return true if the key existed + * @throws ConfigDbException + */ + public synchronized boolean deleteApiKey ( String key ) throws ConfigDbException + { + return fDb.clear ( makePath ( key ) ); + } + + private final JsonConfigDb fDb; + private final ConfigPath fBasePath; + private final NsaApiKeyFactory fKeyFactory; + + private static final String kStdRootPath = "/apikeys"; + + private ConfigPath makePath ( String key ) + { + return fBasePath.getChild ( key ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/CassandraAuthDb.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/CassandraAuthDb.java new file mode 100644 index 0000000..1610b6e --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/CassandraAuthDb.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +import com.att.nsa.security.NsaApiKey; +import com.datastax.driver.core.BoundStatement; +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ConsistencyLevel; +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.QueryOptions; +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.SocketOptions; +import com.datastax.driver.core.policies.ConstantReconnectionPolicy; +import com.datastax.driver.core.policies.RoundRobinPolicy; + +@Deprecated +public class CassandraAuthDb implements NsaAuthDb { + + private final Cluster cluster; + private final Session session; + private final ConcurrentHashMap preparedStatements; + private final Object prepareStatementCreateLock; + private final List contactPoints; + private final int port; + + private enum StatementName { + CREATE_RESOURCE, + GET_ACL, + PERMIT, + DENY + } + + @SuppressWarnings("unused") //Hide the implicit constructor + private CassandraAuthDb() { + this.cluster = null; + this.session = null; + this.preparedStatements = null; + this.prepareStatementCreateLock = null; + this.port = -1; + this.contactPoints = null; + } + + public CassandraAuthDb(List contactPoints, int port) { + + this.contactPoints = new ArrayList (contactPoints.size()); + + for (String contactPoint : contactPoints) { + try { + this.contactPoints.add(InetAddress.getByName(contactPoint)); + } catch (UnknownHostException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + this.port = port; + + cluster = (new Cluster.Builder()).withPort (this.port) + .addContactPoints(this.contactPoints) + .withSocketOptions(new SocketOptions().setReadTimeoutMillis(60000).setKeepAlive(true).setReuseAddress(true)) + .withLoadBalancingPolicy(new RoundRobinPolicy()) + .withReconnectionPolicy(new ConstantReconnectionPolicy(500L)) + .withQueryOptions(new QueryOptions().setConsistencyLevel(ConsistencyLevel.ONE)) + .build (); + + session = cluster.newSession(); + preparedStatements = new ConcurrentHashMap (); + prepareStatementCreateLock = new Object(); + } + + private void createKeyspaceIfNotExists() { + session.execute("CREATE KEYSPACE IF NOT EXISTS fe3c WITH replication = {'class':'SimpleStrategy', 'replication_factor': '1'}"); + } + + private void createTableIfNotExists() { + session.execute("CREATE TABLE IF NOT EXISTS fe3c.authorizations (resource text, role text, user text, PRIMARY KEY(resource, role, user));"); + } + + private void prepareStatements() { + + createKeyspaceIfNotExists(); + createTableIfNotExists(); + + preparedStatements.put(StatementName.CREATE_RESOURCE, session.prepare("INSERT INTO fe3c.authorizations (resource, role, user) VALUES (?, 'OWNER', ?) IF NOT EXISTS")); + preparedStatements.put(StatementName.GET_ACL, session.prepare("SELECT * FROM fe3c.authorizations WHERE resource = ?")); + preparedStatements.put(StatementName.DENY, session.prepare("DELETE FROM fe3c.authorizations WHERE resource = ? AND role = 'USER' AND user = ?")); + preparedStatements.put(StatementName.PERMIT, session.prepare("INSERT INTO fe3c.authorizations (resource, role, user) VALUES(?, 'USER', ?)")); + } + + private PreparedStatement getStatement(StatementName name) { + if (preparedStatements.isEmpty()) { + synchronized (prepareStatementCreateLock) { + if (preparedStatements.isEmpty()) { + prepareStatements(); + } + } + } + + return preparedStatements.get(name); + } + + @Override + public boolean isAuthorized(K key, String resource, String operation) + throws AuthorizationServiceUnavailableException { + final BoundStatement permitStatement = new BoundStatement(getStatement(StatementName.GET_ACL)); + permitStatement.bind(resource); + + final ResultSet results = session.execute(permitStatement); + + final String comparisonKey = (key == null) ? "" : key.getKey(); + + for (Row result : results) { + if (result.getString("user").equals(comparisonKey) || result.getString("user").equals("*")) { + return true; + } + } + + return false; + } + + @Override + public void createResource(K owner, String resource) throws AuthorizationServiceUnavailableException { + final BoundStatement createStatement = new BoundStatement(getStatement(StatementName.CREATE_RESOURCE)); + + createStatement.bind(resource, owner.getKey()); + + session.execute(createStatement); + } + + @Override + public void permit(K key, String resource) { + final BoundStatement permitStatement = new BoundStatement(getStatement(StatementName.PERMIT)); + + permitStatement.bind(resource, (key == null) ? "" : key.getKey()); + + session.execute(permitStatement); + } + + @Override + public void deny(K key, String resource) { + final BoundStatement denyStatement = new BoundStatement(getStatement(StatementName.DENY)); + + denyStatement.bind(resource, (key == null) ? "" : key.getKey()); + + session.execute(denyStatement); + } + + @Override + public void permitAll(String resource) { + final BoundStatement permitStatement = new BoundStatement(getStatement(StatementName.PERMIT)); + + permitStatement.bind(resource, "*"); + + session.execute(permitStatement); + } + + @Override + public void denyAll(String resource) { + final BoundStatement denyStatement = new BoundStatement(getStatement(StatementName.DENY)); + + denyStatement.bind(resource, "*"); + + session.execute(denyStatement); + } + +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/EncryptingApiDbImpl.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/EncryptingApiDbImpl.java new file mode 100644 index 0000000..1994c75 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/EncryptingApiDbImpl.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import java.security.Key; + +import com.att.nsa.configs.ConfigDb; +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.confimpl.EncryptingLayer; +import com.att.nsa.security.NsaApiKey; + +/** + * An extension of the base API Key DB implementation that uses an encrypting layer above the + * provided config db. The provided db can be any implementation, this layer just adds + * encryption to the values. Note that keys are visible, just values are encrypted. + * + * @author peter + * + * @param + */ +public class EncryptingApiDbImpl extends BaseNsaApiDbImpl +{ + public static final String kEncAlgo = "AES/CBC/PKCS5Padding"; + + /** + * Construct an encrypting wrapper around a given config db. The app's secret key and + * initialization vector must be consistent across instantiations. Loss of either will + * prevent recovery of stored data. + * + * @param db + * @param keyFactory + * @param appSecretKey The secret key for this data. This must be an AES compatible key. + * @param iv The initializaton vector for the Cipher used in this implementation. + * @throws ConfigDbException + */ + public EncryptingApiDbImpl ( ConfigDb db, NsaApiKeyFactory keyFactory, Key appSecretKey, byte[] iv ) throws ConfigDbException + { + super ( wrap ( db, appSecretKey, iv ), keyFactory ); + } + + /** + * Construct an encrypting wrapper around a given config db. The app's secret key and + * initialization vector must be consistent across instantiations. Loss of either will + * prevent recovery of stored data. + * + * @param db + * @param rootPath + * @param keyFactory + * @param appSecretKey The secret key for this data. This must be an AES compatible key. + * @param iv The initializaton vector for the Cipher used in this implementation. + * @throws ConfigDbException + */ + public EncryptingApiDbImpl ( ConfigDb db, String rootPath, NsaApiKeyFactory keyFactory, Key appSecretKey, byte[] iv ) throws ConfigDbException + { + super ( wrap ( db, appSecretKey, iv ), rootPath, keyFactory ); + } + + private static ConfigDb wrap ( ConfigDb storage, Key key, byte[] iv ) + { + return new EncryptingLayer ( storage, kEncAlgo, key, iv ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiDb.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiDb.java new file mode 100644 index 0000000..4688fd3 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiDb.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import java.util.Map; +import java.util.Set; + +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.NsaSecurityManagerException; + +/** + * Persistent storage for API keys and secrets built over an abstract config db. Instances + * of this DB must support concurrent access. + */ +public interface NsaApiDb +{ + /** + * Load all keys known to this database. (This could be expensive.) + * @return a set of all API keys + * @throws ConfigDbException + */ + Set loadAllKeys () throws ConfigDbException; + + /** + * Load all keys known to this database. (This could be expensive.) + * @return a map of api key to the api key record + * @throws ConfigDbException + */ + Map loadAllKeyRecords () throws ConfigDbException; + + /** + * Load an API key record based on the API key value + * @param apiKey + * @return an API key record or null + * @throws ConfigDbException + */ + K loadApiKey ( String apiKey ) throws ConfigDbException; + + /** + * Save an API key record. This must be used after changing auxiliary data on the record. + * Note that the key must exist (via createApiKey). + * @param key + * @throws ConfigDbException + */ + void saveApiKey ( K apiKey ) throws ConfigDbException; + + /** + * Create a new API key. If one exists, + * @param key + * @param sharedSecret + * @return the new API key record + * @throws ConfigDbException + */ + K createApiKey ( String key, String sharedSecret ) throws KeyExistsException, ConfigDbException; + + /** + * Delete an API key; equivalent to deleteApiKey ( key.getKey() ) + * @param key + * @return true if the key existed + * @throws ConfigDbException + */ + boolean deleteApiKey ( K key ) throws ConfigDbException; + + /** + * Delete an API key from storage. + * @param key + * @return true if the key existed + * @throws ConfigDbException + */ + boolean deleteApiKey ( String key ) throws ConfigDbException; + + /** + * An exception to signal a key already exists + */ + public static class KeyExistsException extends NsaSecurityManagerException + { + public KeyExistsException ( String key ) { super ( "API key " + key + " exists" ); } + private static final long serialVersionUID = 1L; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiKeyFactory.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiKeyFactory.java new file mode 100644 index 0000000..a63bc34 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaApiKeyFactory.java @@ -0,0 +1,24 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import com.att.nsa.security.NsaApiKey; + +public interface NsaApiKeyFactory +{ + /** + * Create a key using a key and secret + * @param key + * @param sharedSecret + * @return the key instance + */ + K makeNewKey ( String key, String sharedSecret ); + + /** + * Create a key from its serialized form + * @param serializedForm + * @return the key instance + */ + K makeNewKey ( String serializedForm ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaAuthDb.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaAuthDb.java new file mode 100644 index 0000000..265398d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/NsaAuthDb.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +import com.att.nsa.security.NsaApiKey; + +/** + * + * @param + * @deprecated use ReadWriteSecuredResource + */ +@Deprecated +public interface NsaAuthDb { + + + /** + * + * @param owner + * @param resource + * @throws AuthorizationServiceUnavailableException + */ + void createResource(K owner, String resource) throws AuthorizationServiceUnavailableException, ResourceExistsException; + + /** + * + * @param key + * @param resource + */ + void permit(K key, String resource) throws AuthorizationServiceUnavailableException; + + /** + * + * @param key + * @param resource + */ + void deny(K key, String resource) throws AuthorizationServiceUnavailableException; + + /** + * + * @param resource + */ + void permitAll(String resource) throws AuthorizationServiceUnavailableException; + + /** + * + * @param resource + */ + void denyAll(String resource) throws AuthorizationServiceUnavailableException; + + /** + * Determines whether the given key is authorized for the (resource, operation) pair. + * @param key An API Key + * @param resource The resource to grant authorization to + * @param operation The operation for which the key is permitted to execute on the resource + * @return true if the key is permitted to perform the operation on the resource, otherwise false + */ + public boolean isAuthorized(K key, String resource, String operation) throws AuthorizationServiceUnavailableException; + +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/ResourceExistsException.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/ResourceExistsException.java new file mode 100644 index 0000000..d7778b5 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/ResourceExistsException.java @@ -0,0 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db; + +public class ResourceExistsException extends Exception { + private static final long serialVersionUID = 1L; + + public ResourceExistsException() { super(); } + public ResourceExistsException(String message) { super(message); } + public ResourceExistsException(Throwable t) { super(t); } + public ResourceExistsException(String message, Throwable t) { super(message, t); } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKey.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKey.java new file mode 100644 index 0000000..8f0b0a0 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKey.java @@ -0,0 +1,155 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db.simple; + +import org.json.JSONObject; + +import com.att.nsa.data.json.SaJsonUtil; +import com.att.nsa.security.NsaApiKey; + +/** + * An API key record, which includes the API key, the API secret, + * and any auxiliary data. + * + * @author peter + * + */ +public class NsaSimpleApiKey implements NsaApiKey +{ + public static final String kApiKeyField = "key"; + public static final String kApiSecretField = "secret"; + public static final String kApiEnabled = "enabled"; + + public static final String kAuxData = "aux"; + public static final String kAuxEmail = "email"; + public static final String kAuxDescription = "description"; + + public NsaSimpleApiKey ( JSONObject data ) + { + fData = data; + + // check for required fields (these throw if not present) + getKey (); + getSecret (); + + // make sure we've got an aux data object + final JSONObject aux = fData.optJSONObject ( kAuxData ); + if ( aux == null ) + { + fData.put ( kAuxData, new JSONObject () ); + } + } + + @Override + public String toString () + { + return getKey() + ": " + fData.toString (); + } + + @Override + public String serialize () + { + return sfPrettyStore ? serializeAsJson().toString ( 4 ) : serializeAsJson().toString (); + } + + public JSONObject serializeAsJson () + { + return SaJsonUtil.clone ( fData ); + } + + public JSONObject asJsonObject () + { + // always remove the secret from the generated json + final JSONObject full = new JSONObject ( fData, JSONObject.getNames ( fData ) ); + full.remove ( kApiSecretField ); + return full; + } + + public NsaSimpleApiKey ( String apiKey, String sharedSecret ) + { + fData = new JSONObject (); + fData.put ( kAuxData, new JSONObject () ); + + fData.put ( kApiKeyField, apiKey); + fData.put ( kApiSecretField, sharedSecret); + } + + @Override + public String getKey () + { + return fData.getString ( kApiKeyField ); + } + + @Override + public String getSecret () + { + return fData.getString ( kApiSecretField ); + } + + /** + * This normally shouldn't be used, as clients would normally just create a new + * API key. However, accidents happen. + * @param sharedSecret + */ + public void resetSecret ( String sharedSecret ) + { + fData.put ( kApiSecretField, sharedSecret); + } + + public void enable () + { + fData.put ( kApiEnabled, true ); + } + + public void disable () + { + fData.put ( kApiEnabled, false ); + } + + public boolean enabled () + { + return fData.optBoolean ( kApiEnabled, true ); + } + + public void setContactEmail ( String contactEmail ) + { + set ( kAuxEmail, contactEmail ); + } + + public String getContactEmail () + { + return get ( kAuxEmail, "" ); + } + + public void setDescription ( String description ) + { + set ( kAuxDescription, description ); + } + + public String getDescription () + { + return get ( kAuxDescription, "" ); + } + + @Override + public void set ( String key, String val ) + { + fData.getJSONObject ( kAuxData ).put ( key, val ); + } + + @Override + public String get ( String key ) + { + return get ( key, null ); + } + + @Override + public String get ( String key, String defval ) + { + return fData.getJSONObject ( kAuxData ).optString ( key, defval ); + } + + private final JSONObject fData; + private static final boolean sfPrettyStore = Boolean.parseBoolean ( System.getProperty ( "configdb.pretty", "false" ) ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKeyFactory.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKeyFactory.java new file mode 100644 index 0000000..1f86e60 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleApiKeyFactory.java @@ -0,0 +1,28 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db.simple; + +import org.json.JSONObject; + +import com.att.nsa.security.db.NsaApiKeyFactory; + +/** + * A factory for the simple API key implementation + * @author peter + * + */ +public class NsaSimpleApiKeyFactory implements NsaApiKeyFactory +{ + @Override + public NsaSimpleApiKey makeNewKey ( String key, String sharedSecret ) + { + return new NsaSimpleApiKey ( key, sharedSecret ); + } + + @Override + public NsaSimpleApiKey makeNewKey ( String serializedForm ) + { + return new NsaSimpleApiKey ( new JSONObject ( serializedForm ) ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleRemoteApiKey.java b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleRemoteApiKey.java new file mode 100644 index 0000000..ee6dc16 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/security/db/simple/NsaSimpleRemoteApiKey.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.db.simple; + +import org.json.JSONObject; + +public class NsaSimpleRemoteApiKey extends NsaSimpleApiKey +{ + public NsaSimpleRemoteApiKey ( JSONObject data ) + { + super ( data ); + } + + @Override + public String getSecret () + { + throw new IllegalStateException ( "This is a read-only API key that does not carry a secret." ); + } + + @Override + public void enable () + { + throw new IllegalStateException ( "This is a read-only API key." ); + } + + @Override + public void disable () + { + throw new IllegalStateException ( "This is a read-only API key." ); + } + + @Override + public void set ( String key, String val ) + { + throw new IllegalStateException ( "This is a read-only API key." ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/UiPlugin.java b/saserverlibrary/src/main/java/com/att/nsa/ui/UiPlugin.java new file mode 100644 index 0000000..0f0574d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/UiPlugin.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui; + +import java.util.List; + +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.till.nv.rrNvReadable; + +/** + * A plug-in for the UI framework + * @author peter + */ +public interface UiPlugin +{ + /** + * Get the name of this plugin + * @return + */ + public String getUiName (); + + /** + * Get the base location for this plugin + * @return + */ + public String getUiLink (); + + /** + * Get the CSS files for this plugin. The system's standard CSS path is applied, so these + * would be relative to that path. + * @return a list of 0 or more CSS files to load on every page + */ + public List getUiCssList (); + + /** + * Configure this plugin with the given settings. + * @param settings + */ + public void configure ( rrNvReadable settings ); + + /** + * Setup routing into this plugin. Routing paths must start with + * the location given by getUiLink() + * @param router + */ + public void setupRouting ( DrumlinRequestRouter router ); + + /** + * When the servlet engine creates a new session, this method is called on all plugins. + * @param s + */ + public void onNewSession ( UiSession s ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/UiServlet.java b/saserverlibrary/src/main/java/com/att/nsa/ui/UiServlet.java new file mode 100644 index 0000000..b266dca --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/UiServlet.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui; + +import java.io.IOException; +import java.net.URL; +import java.util.LinkedList; + +import javax.servlet.ServletException; + +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.runtime.RuntimeConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.drumlin.service.framework.DrumlinConnection; +import com.att.nsa.drumlin.service.framework.DrumlinServlet; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.service.framework.routing.playish.DrumlinPlayishRoutingFileSource; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.ui.velocity.UiVelocityEventHandler; +import com.att.nsa.ui.velocity.UiVelocityResourceLoader; + +/** + * A servlet for UI sessions that are composed of plugins. + * @author peter + * + */ +public class UiServlet extends DrumlinServlet +{ + public UiServlet ( rrNvReadable settings ) + { + super ( settings, null, sessionLifeCycle.FULL_SESSION ); + fPlugins = new LinkedList (); + + // this has to come before the servletSetup() call due to Drumlin setting + // velocity prior to that call + fVeloLoader = new UiVelocityResourceLoader (); + } + + /** + * Plugins must be registered before servletSetup() is called, so likely + * at some time between construction and "start" on the servlet container. + * @param p + */ + public void register ( UiPlugin p ) + { + fPlugins.add ( p ); + } + + @Override + public final DrumlinConnection createSession () throws rrNvReadable.missingReqdSetting + { + final UiSession s = new UiSession (); + for ( UiPlugin p : fPlugins ) + { + p.onNewSession ( s ); + } + return s; + } + + @Override + protected final void servletSetup () throws rrNvReadable.missingReqdSetting, rrNvReadable.invalidSettingValue, ServletException + { + try + { + final rrNvReadable settings = super.getSettings (); + for ( UiPlugin p : fPlugins ) + { + p.configure ( settings ); + } + + final DrumlinRequestRouter router = getRequestRouter(); + + // let plugins setup routes first so that they can't override the main servlet routes + for ( UiPlugin p : fPlugins ) + { + p.setupRouting ( router ); + } + + // put the list of plugins into the base context for menu rendering + addToBaseContext ( "plugins", fPlugins ); + + // now do main routes + final URL url = UiServlet.class.getResource ( "/uiRoutes.conf" ); + router.addRouteSource ( new DrumlinPlayishRoutingFileSource ( url ) ); + + // all set + log.info ( "UI Servlet setup" ); + } + catch ( IOException e ) + { + throw new rrNvReadable.invalidSettingValue ( "", e ); + } + } + + @Override + protected void setupResourceLoader ( VelocityEngine ve, rrNvReadable p ) + { + // use a custom resource loader that knows about apps + ve.setProperty ( RuntimeConstants.RESOURCE_LOADER, "uiloader" ); + ve.setProperty ( "uiloader.resource.loader.instance", fVeloLoader ); + + // also register an event handler + ve.setProperty ( "eventhandler.include.class", UiVelocityEventHandler.class.getName() ); + } + + private final LinkedList fPlugins; + private UiVelocityResourceLoader fVeloLoader; + + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger ( UiServlet.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/UiSession.java b/saserverlibrary/src/main/java/com/att/nsa/ui/UiSession.java new file mode 100644 index 0000000..524e477 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/UiSession.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import javax.servlet.ServletException; + +import com.att.nsa.drumlin.service.framework.DrumlinConnection; +import com.att.nsa.drumlin.service.framework.DrumlinConnectionContext; +import com.att.nsa.drumlin.service.framework.DrumlinServlet; +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class UiSession implements DrumlinConnection +{ + public UiSession () + { + fMsgs = new LinkedList (); + } + + public enum Level + { + INFO, + WARNING + } + + public static class Message + { + public Message ( Level level, String msg ) + { + fLevel = level; + fMsg = msg; + } + public String getMessage () { return fMsg; } + public Level getLevel () { return fLevel; } + private final String fMsg; + private final Level fLevel; + } + + /** + * Convenience method to downcast a session to a UiSession. + * Will throw ClassCastExcpetion if the session is not the correct type. + * @param ctx + * @return + */ + public static UiSession getUiSession ( DrumlinRequestContext ctx ) + { + return (UiSession) ctx.session (); + } + + public void setStatusMessage ( Message m ) + { + synchronized ( fMsgs ) + { + fMsgs.add ( m ); + } + } + + public List getMessages () + { + synchronized ( fMsgs ) + { + final LinkedList result = new LinkedList (); + result.addAll ( fMsgs ); + fMsgs.clear (); + return result; + } + } + + @Override + public void onSessionCreate ( DrumlinServlet ws, DrumlinConnectionContext dcc ) throws ServletException + { + } + + @Override + public void onSessionClose () + { + } + + @Override + public void noteActivity () + { + } + + @Override + public void buildTemplateContext ( HashMap context ) + { + context.put ( "messages", getMessages() ); + } + + public void put ( String key, Object obj ) + { + fObjects.put ( key, obj ); + } + + public void clear ( String key ) + { + fObjects.remove ( key ); + } + + public Object get ( String key ) + { + return fObjects.get ( key ); + } + + private HashMap fObjects = new HashMap (); + private LinkedList fMsgs = new LinkedList (); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/UiTomcatServer.java b/saserverlibrary/src/main/java/com/att/nsa/ui/UiTomcatServer.java new file mode 100644 index 0000000..3995842 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/UiTomcatServer.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ + +package com.att.nsa.ui; + +import java.io.File; +import java.io.IOException; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; + +import com.att.nsa.drumlin.till.nv.rrNvReadable.loadException; +import com.att.nsa.drumlin.till.nv.rrNvReadable.missingReqdSetting; +import com.att.nsa.drumlin.till.nv.impl.nvWriteableTable; + +public class UiTomcatServer +{ + public UiTomcatServer () throws IOException + { + this ( 8080 ); + } + + public UiTomcatServer ( int port ) throws IOException + { + // create a tomcat instance. Note that the base dir must be set before anything + // else is done. + fTomcat = new Tomcat (); + fTomcat.setBaseDir ( makeTmpDir ( "tomcatBase" ).getAbsolutePath () ); + + fServlet = new UiServlet ( new nvWriteableTable () ); + + fPort = port; + } + + public void addPlugin ( UiPlugin p ) + { + fServlet.register ( p ); + } + + public void setPort ( int port ) + { + fPort = port; + } + + public void start () throws IOException + { + // create a servlet and context + final Context rootCtx = fTomcat.addContext ( "", makeTmpDir ( "uiContext" ).getAbsolutePath () ); + Tomcat.addServlet ( rootCtx, kServletName, fServlet ); + rootCtx.addServletMapping ( "/*", kServletName ); + + // determine the port + fTomcat.setPort ( fPort ); + + try + { + fTomcat.start (); + fTomcat.getServer().await (); + } + catch ( Exception e ) + { + throw new RuntimeException ( e ); + } + } + + public void stop () + { + try + { + fTomcat.stop (); + } + catch ( LifecycleException e ) + { + // ignore + } + } + + private final Tomcat fTomcat; + private final UiServlet fServlet; + private int fPort; + + private static final String kServletName = "UiServlet"; + + public static void main ( String[] args ) throws IOException, loadException, missingReqdSetting + { + int port = 8080; + if ( args.length > 0 ) + { + port = Integer.parseInt ( args[0] ); + } + + final UiTomcatServer t = new UiTomcatServer ( port ); + t.start (); + } + + private static File makeTmpDir ( String name ) throws IOException + { + final File temp = File.createTempFile ("nsaui." + name + ".", Long.toString ( System.nanoTime() ) ); + if ( !temp.delete () ) + { + throw new IOException ( "Couldn't delete tmp file " + temp.getAbsolutePath () ); + } + if ( !temp.mkdir () ) + { + throw new IOException ( "Couldn't create tmp dir " + temp.getAbsolutePath () ); + } + temp.deleteOnExit (); + return temp; + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/MetricsUi.java b/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/MetricsUi.java new file mode 100644 index 0000000..48b40c9 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/MetricsUi.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui.endpoints; + +import java.io.IOException; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class MetricsUi +{ + public static void getMetricsMain ( DrumlinRequestContext ctx ) + { +// final UiServlet servlet = ((UiServlet) ctx.getServlet ()); +// +// final HpProcessingEngine config = servlet.getConfig (); +// ctx.renderer ().put ( "config", config ); +// +// final ConfigurableCorrelationEngine cce = servlet.getCce (); +// ctx.renderer ().put ( "cce", cce ); +// +// final CdmMetricsRegistry m = servlet.getMetrics (); +// ctx.renderer ().put ( "metrics", m ); +// +// // put some organization around hierarchical naming +// ctx.renderer ().put ( "metricEntries", m.getEntries () ); +// +// ctx.renderer ().renderTemplate ( "metrics.html" ); + } + + public static void getAllMetrics ( DrumlinRequestContext ctx ) throws IOException + { +// final UiServlet servlet = ((UiServlet) ctx.getServlet ()); +// final CdmMetricsRegistry m = servlet.getMetrics (); +// +// final String format = ctx.request ().getParameter ( "format", "json" ).trim ().toLowerCase (); +// if ( format.equals ( "json" ) ) +// { +// final JSONObject o = new JSONObject (); +// for ( Entry mi : m.getItems ().entrySet () ) +// { +// o.put ( mi.getKey(), mi.getValue().getRawValueString () ); +// } +// ctx.response ().sendErrorAndBody ( HttpStatusCodes.k200_ok, o.toString(), MimeTypes.kAppJson ); +// } +// else if ( format.equals ( "xml" ) ) +// { +// // XML +// ctx.response ().setStatus ( HttpStatusCodes.k200_ok ); +// ctx.response ().setContentType ( MimeTypes.kAppXml ); +// final PrintWriter pw = ctx.response ().getStreamForTextResponse (); +// pw.println ( "" ); +// pw.println ( "" ); +// for ( Entry mi : m.getItems ().entrySet () ) +// { +// pw.println ( "" + mi.getKey() + "" + mi.getValue().getRawValueString () + "" ); +// } +// pw.println ( "" ); +// pw.close (); +// } +// else +// { +// // plain text +// ctx.response ().setStatus ( HttpStatusCodes.k200_ok ); +// ctx.response ().setContentType ( MimeTypes.kPlainText ); +// final PrintWriter pw = ctx.response ().getStreamForTextResponse (); +// for ( Entry mi : m.getItems ().entrySet () ) +// { +// pw.println ( mi.getKey() + "," + mi.getValue().getRawValueString () ); +// } +// pw.close (); +// } +// } +// +// public static void getMetric ( DrumlinRequestContext ctx, String id ) +// { +// final UiServlet servlet = ((UiServlet) ctx.getServlet ()); +// +// final CdmMetricsRegistry m = servlet.getMetrics (); +// final CdmMeasuredItem mi = m.getItem ( id ); +// if ( mi == null ) +// { +// ctx.response ().sendError ( HttpStatusCodes.k404_notFound, "metrics [" + id + "] does not exist" ); +// } +// else +// { +// ctx.response().sendErrorAndBody ( HttpStatusCodes.k200_ok, mi.getRawValueString ().toString(), MimeTypes.kPlainText ); +// } + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/UiMain.java b/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/UiMain.java new file mode 100644 index 0000000..fa49ff4 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/endpoints/UiMain.java @@ -0,0 +1,14 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui.endpoints; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; + +public class UiMain +{ + public static void getMain ( DrumlinRequestContext ctx ) + { + ctx.renderer ().renderTemplate ( "/templates/main.html" ); + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/plugins/UiMetricsPlugin.java b/saserverlibrary/src/main/java/com/att/nsa/ui/plugins/UiMetricsPlugin.java new file mode 100644 index 0000000..ce01604 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/plugins/UiMetricsPlugin.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui.plugins; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +import org.json.JSONObject; + +import com.att.nsa.drumlin.service.framework.context.DrumlinRequestContext; +import com.att.nsa.drumlin.service.framework.rendering.DrumlinRenderContext; +import com.att.nsa.drumlin.service.framework.routing.DrumlinRequestRouter; +import com.att.nsa.drumlin.service.framework.routing.playish.DrumlinPlayishInstanceCallRoutingSource; +import com.att.nsa.drumlin.service.standards.HttpStatusCodes; +import com.att.nsa.drumlin.service.standards.MimeTypes; +import com.att.nsa.drumlin.till.nv.rrNvReadable; +import com.att.nsa.metrics.CdmMeasuredItem; +import com.att.nsa.metrics.CdmMetricsRegistry; +import com.att.nsa.ui.UiPlugin; +import com.att.nsa.ui.UiSession; + +public class UiMetricsPlugin implements UiPlugin +{ + public void setMetrics ( CdmMetricsRegistry metrics ) + { + fMetrics = metrics; + } + + @Override + public String getUiName () + { + return "metrics"; + } + + @Override + public String getUiLink () + { + return "/metrics"; + } + + @Override + public List getUiCssList () + { + return new LinkedList (); + } + + @Override + public void configure ( rrNvReadable settings ) + { + // TODO Auto-generated method stub + + } + + public void setupRouting ( DrumlinRequestRouter router ) + { + final DrumlinPlayishInstanceCallRoutingSource src = new DrumlinPlayishInstanceCallRoutingSource ( this ); + src.addRoute ( "get", "/metrics", this.getClass().getName() + ".getAll" ); + src.addRoute ( "get", "/metrics/clear", this.getClass().getName() + ".clear" ); + src.addRoute ( "get", "/api/metrics", this.getClass().getName() + ".getAllMetrics" ); + src.addRoute ( "get", "/api/metrics/{id}", this.getClass().getName() + ".getMetric" ); + router.addRouteSource ( src ); + } + + public void getAll ( DrumlinRequestContext ctx ) + { + final DrumlinRenderContext renderer = ctx.renderer(); + + renderer.put ( "metrics", fMetrics ); + renderer.put ( "metricEntries", fMetrics.getEntries () ); + + renderer.renderTemplate ( "/templates/metrics.html" ); + } + + public void clear ( DrumlinRequestContext ctx ) + { + // FIXME: there's no reset mechanism! + + getAll ( ctx ); + } + + public void getAllMetrics ( DrumlinRequestContext ctx ) throws IOException + { + final String format = ctx.request ().getParameter ( "format", "json" ).trim ().toLowerCase (); + if ( format.equals ( "json" ) ) + { + final JSONObject o = new JSONObject (); + for ( Entry mi : fMetrics.getItems ().entrySet () ) + { + o.put ( mi.getKey(), mi.getValue().getRawValueString () ); + } + ctx.response ().sendErrorAndBody ( HttpStatusCodes.k200_ok, o.toString(), MimeTypes.kAppJson ); + } + else if ( format.equals ( "xml" ) ) + { + // XML + ctx.response ().setStatus ( HttpStatusCodes.k200_ok ); + ctx.response ().setContentType ( MimeTypes.kAppXml ); + final PrintWriter pw = ctx.response ().getStreamForTextResponse (); + pw.println ( "" ); + pw.println ( "" ); + for ( Entry mi : fMetrics.getItems ().entrySet () ) + { + pw.println ( "" + mi.getKey() + "" + mi.getValue().getRawValueString () + "" ); + } + pw.println ( "" ); + pw.close (); + } + else + { + // plain text + ctx.response ().setStatus ( HttpStatusCodes.k200_ok ); + ctx.response ().setContentType ( MimeTypes.kPlainText ); + final PrintWriter pw = ctx.response ().getStreamForTextResponse (); + for ( Entry mi : fMetrics.getItems ().entrySet () ) + { + pw.println ( mi.getKey() + "," + mi.getValue().getRawValueString () ); + } + pw.close (); + } + } + + public void getMetric ( DrumlinRequestContext ctx, String id ) + { + final CdmMeasuredItem mi = fMetrics.getItem ( id ); + if ( mi == null ) + { + ctx.response ().sendError ( HttpStatusCodes.k404_notFound, "metrics [" + id + "] does not exist" ); + } + else + { + ctx.response().sendErrorAndBody ( HttpStatusCodes.k200_ok, mi.getRawValueString ().toString(), MimeTypes.kPlainText ); + } + } + + @Override + public void onNewSession ( UiSession s ) + { + } + + private CdmMetricsRegistry fMetrics; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityEventHandler.java b/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityEventHandler.java new file mode 100644 index 0000000..3abd26b --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityEventHandler.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui.velocity; + +import org.apache.velocity.app.event.IncludeEventHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UiVelocityEventHandler implements IncludeEventHandler +{ + /** + * Called when an include-type directive is encountered ( + * #include or #parse). May modify the path + * of the resource to be included or may block the include entirely. All the + * registered IncludeEventHandlers are called unless null is returned. If + * none are registered the template at the includeResourcePath is retrieved. + * + * @param includeResourcePath the path as given in the include directive. + * @param currentResourcePath the path of the currently rendering template that includes the + * include directive. + * @param directiveName name of the directive used to include the resource. (With the + * standard directives this is either "parse" or "include"). + * + * @return a new resource path for the directive, or null to block the + * include from occurring. + */ + public String includeEvent ( String includeResourcePath, String currentResourcePath, String directiveName ) + { + // in this scheme, the system always sees the included path as relative, because + // when current is prefixed with the app tag, it's not thought to be an absolute path. + // So velocity will go to the last slash of current, then append include. If current doesn't + // contain a slash, it'll use includeResourcePath + + String result = includeResourcePath; + final String appId = UiVelocityResourceLoader.getAppPrefix ( currentResourcePath ); + if ( appId != null && !currentResourcePath.contains ( "/" ) ) + { + result = UiVelocityResourceLoader.encodeTemplateName ( appId, includeResourcePath ); + } + + log.info ( "UiVelocityEventHandler.includeEvent ( " + includeResourcePath + ", " + + currentResourcePath + ", " + directiveName + " ) --> " + result ); + return result; + } + + private static final Logger log = LoggerFactory.getLogger ( UiVelocityEventHandler.class ); +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityResourceLoader.java b/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityResourceLoader.java new file mode 100644 index 0000000..98c8676 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/ui/velocity/UiVelocityResourceLoader.java @@ -0,0 +1,144 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.ui.velocity; + +import java.io.InputStream; +import java.util.LinkedList; +import java.util.List; + +import org.apache.commons.collections.ExtendedProperties; +import org.apache.commons.io.FilenameUtils; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.RuntimeServices; +import org.apache.velocity.runtime.resource.Resource; +import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; +import org.apache.velocity.runtime.resource.loader.ResourceLoader; + +public class UiVelocityResourceLoader extends ResourceLoader +{ + // the magic string should not include a slash so that velocity's "relative + // path" handler won't see the resulting resource name as having a directory + private static final String kMagicSeparatorString = ":"; + + public static String getAppPrefix ( String name ) + { + final int magic = name.indexOf ( kMagicSeparatorString ); + if ( magic >= 0 ) + { + final String appId = name.substring ( 0, magic ); + return appId; + } + return null; + } + + public static String getBaseName ( String name ) + { + final String prefix = getAppPrefix ( name ); + if ( prefix != null ) + { + return name.substring ( prefix.length() + 1 ); + } + else + { + return name; + } + } + + public static String encodeTemplateName ( String appId, String baseName ) + { + if ( getAppPrefix ( baseName ) != null ) return baseName; + return ( appId == null ? "" : appId + kMagicSeparatorString ) + baseName; + } + + public static List decodeTemplateName ( String name ) + { + final LinkedList list = new LinkedList (); + final int magic = name.indexOf ( kMagicSeparatorString ); + if ( magic >= 0 ) + { + final String appId = name.substring ( 0, magic ); + final String template = name.substring ( magic+kMagicSeparatorString.length () ); + list.add ( "templates/" + appId + "/" + template ); + list.add ( "templates/" + template ); + } + else + { + list.add ( "templates/" + name ); + } + return list; + } + + public UiVelocityResourceLoader () + { + fNormalLoader = new ClasspathResourceLoader (); + } + + @Override + public void commonInit ( RuntimeServices rs, ExtendedProperties configuration) + { + super.commonInit ( rs, configuration ); + fNormalLoader.commonInit ( rs, configuration ); + } + + @Override + public void init ( ExtendedProperties configuration ) + { + fNormalLoader.init ( configuration ); + } + + @Override + public InputStream getResourceStream ( String source ) throws ResourceNotFoundException + { + InputStream result = null; + + final String origSource = source; + + // build a normalized version of the same thing + String base = getBaseName ( source ); + if ( !base.equals ( source ) ) + { + // then it had an app prefix + final String normal = FilenameUtils.normalize ( base ); + source = encodeTemplateName ( getAppPrefix ( source ), normal ); + } + + log.info ( "velocity requests [" + origSource + "], normalized as [" + source + "]..." ); + + final List options = decodeTemplateName ( source ); + for ( String option : options ) + { + log.info ( "\ttrying " + option ); + if ( fNormalLoader.resourceExists ( option ) ) + { + result = fNormalLoader.getResourceStream ( option ); + break; + } + } + + if ( result == null ) + { + log.info ( "\ttrying " + origSource + " (the original)" ); + result = fNormalLoader.getResourceStream ( origSource ); + } + + log.info ( "UiVelocityResourceLoader.getResourceStream ( " + origSource + " ) --> " + + ( result == null ? "" : "found" ) ); + + return result; + } + + @Override + public boolean isSourceModified ( Resource resource ) + { + return fNormalLoader.isSourceModified ( resource ); + } + + @Override + public long getLastModified ( Resource resource ) + { + return fNormalLoader.getLastModified ( resource ); + } + + private final ClasspathResourceLoader fNormalLoader; +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkLock.java b/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkLock.java new file mode 100644 index 0000000..f7f9f04 --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkLock.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package com.att.nsa.zkUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeoutException; + +import org.I0Itec.zkclient.ZkClient; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.KeeperException.NoNodeException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.cmdLine.NsaCommandLineUtil; + +public class ZkLock +{ + public ZkLock ( ZkClient zk, String lockNode ) + { + fZk = zk; + fPath = lockNode; + fLockNode = null; + } + + public void lock ( long timeoutMs ) throws KeeperException, TimeoutException, InterruptedException + { + fZk.createPersistent ( fPath, true ); + + final String myLockNode = fPath + "/" + kLockPrefix; + fLockNode = fZk.createEphemeralSequential ( myLockNode, new byte [0] ); + + final long timeoutAtMs = System.currentTimeMillis() + timeoutMs; + while ( System.currentTimeMillis() < timeoutAtMs ) + { + final List kids = fZk.getChildren(fPath); + final LinkedList sortedKids = new LinkedList (); + for ( String kid : kids ) + { + sortedKids.add ( fPath + "/" + kid ); + } + Collections.sort ( sortedKids ); + + if ( sortedKids.size() == 0 ) + { + throw new NoNodeException ( "Node insertion apparently failed." ); + } + + String lowest = sortedKids.iterator ().next (); + if ( lowest.equals ( fLockNode ) ) + { + // our node is lowest; good to go + log.info ( "Secured ZK lock at [" + fPath + "]." ); + return; + } + + // here, we wait on the next lowest lock from ours. first find it... + String nextLowest = lowest; + for ( String curr : sortedKids ) + { + if ( curr.equals ( fLockNode ) ) + { + break; + } + nextLowest = curr; + } + + // now watch the next guy for deletion + final long remainingTimeMs = Math.max ( 1, timeoutAtMs - System.currentTimeMillis () ); + log.info ( "Waiting for lock [" + fPath + "]; mine is [" + fLockNode + "], waiting for [" + nextLowest + "], up to " + remainingTimeMs + " ms" ); + ZkWatcher.waitForDeletion ( fZk, nextLowest, remainingTimeMs ); + } + + // lock failed + release ( false ); + throw new TimeoutException ( "Couldn't obtain lock on " + fPath + " in time." ); + } + + public void unlock () throws TimeoutException, KeeperException + { + release ( true ); + } + + protected String getLockNode () + { + return fLockNode; + } + + // test program + public static void main ( String[] args ) throws IOException, InterruptedException, KeeperException + { + final Map map = NsaCommandLineUtil.processCmdLine ( args ); + + final String zkaddr = NsaCommandLineUtil.getReqdSetting ( map, "-zk", "the ZK connection string with '-zk'" ); + final ZkClient zk = new ZkClient ( zkaddr, 10000 ); + + try + { + System.out.println (); + System.out.print ( "# " ); + + final HashMap locks = new HashMap (); + + String line = ""; + final BufferedReader br = new BufferedReader ( new InputStreamReader ( System.in ) ); + while ( (line = br.readLine ()) != null ) + { + final String[] parts = line.split ( " " ); + if ( parts.length == 1 && parts[0].equalsIgnoreCase ( "list" ) ) + { + for ( Entry e : locks.entrySet () ) + { + System.out.println ( e.getKey() ); + } + } + else if ( parts.length == 2 && parts[0].equalsIgnoreCase ( "lock" ) ) + { + try + { + final String path = parts[1]; + if ( locks.containsKey ( path ) ) + { + System.out.println ( "i've got that lock" ); + } + else + { + final ZkLock lock = new ZkLock ( zk, parts[1] ); + lock.lock ( 30000 ); + locks.put ( path, lock ); + System.out.println ( "locked" ); + } + } + catch ( TimeoutException e ) + { + System.out.println ( "timed out. no lock." ); + } + catch ( KeeperException e ) + { + e.printStackTrace(); + } + } + else if ( parts.length == 2 && parts[0].equalsIgnoreCase ( "unlock" ) ) + { + try + { + final String path = parts[1]; + if ( locks.containsKey ( path ) ) + { + locks.remove ( path ).unlock (); + System.out.println ( "ok" ); + } + else + { + System.out.println ( "not locked by me" ); + } + } + catch ( TimeoutException e ) + { + e.printStackTrace(); + } + catch ( KeeperException e ) + { + e.printStackTrace(); + } + } + else if ( parts.length == 1 && parts[0].length()==0 ) + { + // nuthin + } + else + { + System.out.println ( "Use 'lock ', 'unlock ' or 'list'" ); + } + + System.out.println (); + System.out.print ( "# " ); + } + } + finally + { + if ( zk != null ) zk.close (); + } + } + + private final ZkClient fZk; + private final String fPath; + private String fLockNode; + private static final String kLockPrefix = "lock-"; + private static final Logger log = LoggerFactory.getLogger ( ZkLock.class ); + + private void release ( boolean locked ) + { + log.info ( "Releasing " + (locked?"lock":"request") + " at [" + fPath + "]." ); + try + { + boolean nodeExisted = fZk.delete ( fLockNode ); + if (!nodeExisted) + { + log.warn ( "While releasing " + (locked?"lock":"request") + " at [" + fPath + "], not wasn't found. (Did you manually remove it?)" ); + } + } + finally + { + fLockNode = null; + } + + } +} diff --git a/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkPath.java b/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkPath.java new file mode 100644 index 0000000..a169a4d --- /dev/null +++ b/saserverlibrary/src/main/java/com/att/nsa/zkUtils/ZkPath.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * Copyright (c) 2016 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package com.att.nsa.zkUtils; + +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.KeeperException.Code; +import org.apache.zookeeper.ZooDefs.Ids; +import org.apache.zookeeper.ZooKeeper; + +public class ZkPath +{ + public static String makeZkPath ( String[] pathParts ) + { + StringBuffer path = new StringBuffer (); + for ( String part : pathParts ) + { + path.append ( "/" ); + path.append ( part ); + } + return path.toString (); + } + + public static String[] appendZkPath ( String[] basePathParts, String part ) + { + final String[] result = new String [ basePathParts.length + 1 ]; + for ( int i=0; i + * + * */ diff --git a/saserverlibrary/src/main/resources/css/print.css b/saserverlibrary/src/main/resources/css/print.css new file mode 100644 index 0000000..b0e9e45 --- /dev/null +++ b/saserverlibrary/src/main/resources/css/print.css @@ -0,0 +1,3 @@ +/* Welcome to Compass. Use this file to define print styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/saserverlibrary/src/main/resources/css/screen.css b/saserverlibrary/src/main/resources/css/screen.css new file mode 100644 index 0000000..a075eaa --- /dev/null +++ b/saserverlibrary/src/main/resources/css/screen.css @@ -0,0 +1,660 @@ +/* line 17, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: 100%; + vertical-align: baseline; +} + +/* line 22, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +html { + line-height: 1; +} + +/* line 24, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +ol, ul { + list-style: none; +} + +/* line 26, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* line 28, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +caption, th, td { + text-align: left; + font-weight: normal; + vertical-align: middle; +} + +/* line 30, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +q, blockquote { + quotes: none; +} +/* line 103, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +q:before, q:after, blockquote:before, blockquote:after { + content: ""; + content: none; +} + +/* line 32, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +a img { + border: none; +} + +/* line 116, ../../../../../../../../../../var/lib/gems/1.9.1/gems/compass-0.12.6/frameworks/compass/stylesheets/compass/reset/_utilities.scss */ +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { + display: block; +} + +/* line 3, ../../sass/sass/_header.scss */ +#topbar-wrap { + width: 100%; + background: white; +} + +/* line 9, ../../sass/sass/_header.scss */ +#topbar { + width: 960px; + margin: 0 auto; + padding-top: 0.75em; + padding-bottom: 0.5em; + position: relative; + /* for absolute positioning internally */ + font-family: Courier, Arial; + /* OmnesATT */ + font-size: 1.0em; + color: #666666; +} +/* line 23, ../../sass/sass/_header.scss */ +#topbar a.selectedTab { + text-decoration: none; + color: #067ab4; +} +/* line 27, ../../sass/sass/_header.scss */ +#topbar a.selectedTab:hover { + color: #44c8f5; + text-decoration: none; +} +/* line 34, ../../sass/sass/_header.scss */ +#topbar a.unselectedTab { + text-decoration: none; + color: #666666; +} +/* line 38, ../../sass/sass/_header.scss */ +#topbar a.unselectedTab:hover { + color: #44c8f5; + text-decoration: none; +} +/* line 45, ../../sass/sass/_header.scss */ +#topbar ul { + list-style: none; +} +/* line 49, ../../sass/sass/_header.scss */ +#topbar ul li { + display: inline; + margin: 0 0.5em 0 0; +} + +/* line 57, ../../sass/sass/_header.scss */ +#searchBarWrap { + border: 2px #aaa solid; + border-radius: 15px; + background-color: #5e5e5e; + padding-left: 0.75em; + padding-right: 0.75em; +} +/* line 66, ../../sass/sass/_header.scss */ +#searchBarWrap input { + border: none; + padding-left: 0.5em; + padding-right: 0.5em; + color: #fff; + background-color: #5e5e5e; +} +/* line 76, ../../sass/sass/_header.scss */ +#searchBarWrap input:focus { + outline: none; +} + +/* line 83, ../../sass/sass/_header.scss */ +.dialog-info { + display: none; + padding-top: 1em; + padding-bottom: 1em; + color: #444; + background-color: #6fbaf7; + text-align: center; +} + +/* line 9, ../../sass/sass/screen.scss */ +body { + -webkit-font-smoothing: antialiased; + font-family: Helvetica, Arial, sans; + font-size: 1em; + line-height: 1.5em; + letter-spacing: 1px; + text-rendering: optimizeLegibility; + background-color: white; + color: #222222; +} + +/* ************************************************************************* */ +/* ************************************************************************* */ +/* line 25, ../../sass/sass/screen.scss */ +#bottom-wrap { + width: 100%; + background: white; + padding-top: 1em; + padding-bottom: 1em; +} + +/* line 33, ../../sass/sass/screen.scss */ +#bottom { + width: 960px; + margin: 0 auto; + position: relative; + font-family: 'Open Sans'; + font-size: 0.8em; + line-height: 1.5em; + color: #333333; +} + +/* line 46, ../../sass/sass/screen.scss */ +.footer-block { + display: inline-block; + vertical-align: top; +} + +/* line 52, ../../sass/sass/screen.scss */ +.footer-text-block { + margin-left: 4em; +} + +/* line 57, ../../sass/sass/screen.scss */ +.footer-text { + margin: 0; + padding: 0; +} +/* line 62, ../../sass/sass/screen.scss */ +.footer-text a { + text-decoration: none; + color: #333333; +} +/* line 67, ../../sass/sass/screen.scss */ +.footer-text a:hover { + text-decoration: underline; +} + +/* ************************************************************************* */ +/* ************************************************************************* */ +/* line 77, ../../sass/sass/screen.scss */ +#body-wrap { + width: 100%; + background-color: white; +} + +/* line 83, ../../sass/sass/screen.scss */ +#body { + width: 960px; + min-height: 500px; + margin: 0 auto; + padding-top: 1em; +} + +/* line 91, ../../sass/sass/screen.scss */ +.dev-note { + font-size: 60%; + border: 1px dashed gray; + background: #eee; + color: #000; + padding-left: 1em; + padding-top: 0.25em; + padding-bottom: 0.25em; +} + +/* line 3, ../../sass/sass/_forms.scss */ +#injectForm { + margin-top: 2em; + width: 100%; + position: relative; +} + +/* line 10, ../../sass/sass/_forms.scss */ +.injectLabel { + display: inline-block; + width: 6em; + text-align: right; + padding-right: 1em; + vertical-align: top; +} + +/* line 21, ../../sass/sass/_forms.scss */ +#alarmData { + wrap: off; + width: 600px; + height: 120px; + border: 3px solid #cccccc; + padding: 0.25em; +} + +/* line 32, ../../sass/sass/_forms.scss */ +.injectError { + color: #ff0000; +} + +/* line 104, ../../sass/sass/screen.scss */ +.title { + font-size: 1.5em; +} + +/* line 109, ../../sass/sass/screen.scss */ +.sectionHeader { + padding-top: 1em; + font-size: 1.25em; +} + +/* line 116, ../../sass/sass/screen.scss */ +.section { + padding-top: 1em; +} +/* line 120, ../../sass/sass/screen.scss */ +.section a { + color: #444488; + text-decoration: none; +} +/* line 125, ../../sass/sass/screen.scss */ +.section a:hover { + color: #666; + text-decoration: none; +} + +/* line 133, ../../sass/sass/screen.scss */ +.sectionFooter { + font-family: Arial; + font-size: 0.7em; +} + +/* line 139, ../../sass/sass/screen.scss */ +.section-control { + margin: 0; +} +/* line 143, ../../sass/sass/screen.scss */ +.section-control:hover { + text-decoration: underline; +} + +/* line 149, ../../sass/sass/screen.scss */ +.section-controlpad { + font-family: Courier; + font-size: 0.8em; +} +/* line 154, ../../sass/sass/screen.scss */ +.section-controlpad a { + color: #444488; + text-decoration: none; +} +/* line 158, ../../sass/sass/screen.scss */ +.section-controlpad a:hover { + color: #666; + text-decoration: none; +} + +/* line 166, ../../sass/sass/screen.scss */ +.control-pad { + padding-top: 0.25em; + padding-bottom: 0.25em; +} +/* line 171, ../../sass/sass/screen.scss */ +.control-pad ul { + list-style: none; +} +/* line 175, ../../sass/sass/screen.scss */ +.control-pad ul li { + display: inline; + margin: 0 0.25em 0 0; +} +/* line 182, ../../sass/sass/screen.scss */ +.control-pad a { + color: #333; + text-decoration: none; +} +/* line 187, ../../sass/sass/screen.scss */ +.control-pad a:hover { + color: #666; + text-decoration: none; +} + +/* line 195, ../../sass/sass/screen.scss */ +.hideable { + display: none; +} + +/* line 208, ../../sass/sass/screen.scss */ +#hpSelectorPad { + display: inline-block; + width: 123px; + padding-right: 2px; + vertical-align: top; + overflow: auto; +} + +/* line 219, ../../sass/sass/screen.scss */ +.hpPage { + display: inline-block; + width: 805px; + vertical-align: top; +} + +/* line 226, ../../sass/sass/screen.scss */ +.settings-list { + margin-top: 1em; + margin-bottom: 1em; + font-family: Courier, Arial; + font-size: 0.8em; +} + +/* line 235, ../../sass/sass/screen.scss */ +.settings-group { + border-bottom: 1px dashed gray; + font-family: Courier, Arial; + /* OmnesATT */ + font-size: 0.7em; +} + +/* line 243, ../../sass/sass/screen.scss */ +.settings-key { + display: inline-block; + width: 225px; + overflow: auto; + vertical-align: top; + font-weight: bold; +} + +/* line 253, ../../sass/sass/screen.scss */ +.settings-block { + display: inline-block; + width: 565px; + overflow: auto; + vertical-align: top; +} + +/* line 261, ../../sass/sass/screen.scss */ +.settings-key-group { + display: inline-block; + vertical-align: top; + width: 495px; +} + +/* line 268, ../../sass/sass/screen.scss */ +.settings-key-group-control { + display: inline-block; + vertical-align: top; + width: 30px; +} + +/* line 287, ../../sass/sass/screen.scss */ +.section-intro { + font-family: Arial; + /* OmnesATT */ + font-size: 0.8em; + line-height: 1.25em; + padding-bottom: 0.5em; +} + +/* line 295, ../../sass/sass/screen.scss */ +.phaseLinkMenu { + margin-top: 1.0em; + font-family: Arial; + font-size: 0.8em; +} + +/* line 303, ../../sass/sass/screen.scss */ +.processing-list { + margin-top: 1em; + margin-bottom: 1em; + font-family: Courier, Arial; +} + +/* line 311, ../../sass/sass/screen.scss */ +.processing-entry { + display: inline-block; + overflow: auto; + vertical-align: top; + padding-top: 1.5em; + padding-bottom: 0.5em; + font-family: Courier, Arial; + font-size: 0.75em; +} + +/* line 333, ../../sass/sass/screen.scss */ +.processing-processor { + margin-left: 3.0em; +} + +/* line 338, ../../sass/sass/screen.scss */ +.processing-processor-even { + background-color: #fff; +} + +/* line 343, ../../sass/sass/screen.scss */ +.processing-processor-odd { + background-color: #eee; +} + +/* line 348, ../../sass/sass/screen.scss */ +.processing-entry-controlPadContainer { + height: 2em; +} + +/* line 353, ../../sass/sass/screen.scss */ +.table { + margin-top: 1.5em; + font-size: 0.8em; +} + +/* line 363, ../../sass/sass/screen.scss */ +.tableLabel { + color: #888; + padding-left: 0.25em; + padding-right: 0.25em; +} +/* line 369, ../../sass/sass/screen.scss */ +.tableLabel a { + text-decoration: none; + color: #444488; +} +/* line 374, ../../sass/sass/screen.scss */ +.tableLabel a:hover { + text-decoration: underline; +} + +/* line 381, ../../sass/sass/screen.scss */ +.tableValue { + color: #000; + padding-left: 0.25em; + padding-right: 0.25em; +} + +/* line 388, ../../sass/sass/screen.scss */ +.corr-list { + margin-top: 1em; + margin-bottom: 1em; + font-family: Arial; + /* OmnesATT */ +} + +/* line 396, ../../sass/sass/screen.scss */ +.cg-label { + color: #444444; +} + +/* line 401, ../../sass/sass/screen.scss */ +.cg-inlineLabel { + display: inline-block; +} + +/* line 406, ../../sass/sass/screen.scss */ +.cg-section { + padding-top: 2.0em; + font-family: Arial; + /* OmnesATT */ + font-size: 1.75em; + font-weight: bold; +} +/* line 414, ../../sass/sass/screen.scss */ +.cg-section a { + text-decoration: none; + color: #222222; +} +/* line 419, ../../sass/sass/screen.scss */ +.cg-section a:hover { + text-decoration: underline; + color: #222222; +} + +/* line 427, ../../sass/sass/screen.scss */ +.cg-detail { + font-size: 0.9em; +} +/* line 431, ../../sass/sass/screen.scss */ +.cg-detail a { + text-decoration: none; + color: #444488; +} +/* line 436, ../../sass/sass/screen.scss */ +.cg-detail a:hover { + text-decoration: underline; +} + +/* line 447, ../../sass/sass/screen.scss */ +.cg-occursOnDetail { + display: inline-block; +} + +/* line 456, ../../sass/sass/screen.scss */ +.cg-triggerdetail { + display: inline-block; +} + +/* line 465, ../../sass/sass/screen.scss */ +.cg-alarmRecordLabel { + display: inline-block; + padding-left: 1.5em; +} + +/* line 471, ../../sass/sass/screen.scss */ +.cg-alarmRecordSpec { + display: inline-block; +} + +/* line 476, ../../sass/sass/screen.scss */ +.cg-alarmRecordTraversal { + display: inline-block; +} + +/* line 481, ../../sass/sass/screen.scss */ +.cg-alarmRecordCritical { + color: #ff0000; +} + +/* line 486, ../../sass/sass/screen.scss */ +.cg-alarmRecordWarning { + color: #fed85d; +} + +/* line 491, ../../sass/sass/screen.scss */ +.cg-parentRecordName { + display: inline-block; + padding-left: 1.5em; +} + +/* line 497, ../../sass/sass/screen.scss */ +.cg-parentRecordTraversal { + display: inline-block; +} + +/* line 510, ../../sass/sass/screen.scss */ +.consAlarmValue { + padding-left: 2em; +} + +/* line 515, ../../sass/sass/screen.scss */ +.consConflictedValue { + color: #f00; +} + +/* line 520, ../../sass/sass/screen.scss */ +.consValueKey { + display: inline-block; + min-width: 5em; +} + +/* line 526, ../../sass/sass/screen.scss */ +.consValueValue { + display: inline-block; + min-width: 10em; +} + +/* line 536, ../../sass/sass/screen.scss */ +.consEvenRow { + background-color: #cec; +} + +/* line 541, ../../sass/sass/screen.scss */ +.consHeaderCell { + padding-left: 0.25em; + padding-right: 1em; + font-weight: bold; +} + +/* line 548, ../../sass/sass/screen.scss */ +.consValueCell { + padding-left: 0.25em; + padding-right: 1em; + font-size: 0.9em; +} + +/* line 555, ../../sass/sass/screen.scss */ +.smaller { + font-size: 0.8em; +} + +/* line 561, ../../sass/sass/screen.scss */ +#editor { + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width: 100%; + height: 100%; + text-align: center; + z-index: 1000; + background-image: url(../images/dimmer.png); +} + +/* line 574, ../../sass/sass/screen.scss */ +#editor .editor-pane { + width: 300px; + margin: 100px auto; + background-color: #fff; + border: 1px solid #000; + padding: 15px; + text-align: center; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.css b/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.css new file mode 100644 index 0000000..eb4127b --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.css @@ -0,0 +1,1566 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('../fonts/fontawesome-webfont.eot?v=4.1.0'); + src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -o-transform: rotate(0deg); + } + 100% { + -o-transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -moz-transform: scale(1, -1); + -ms-transform: scale(1, -1); + -o-transform: scale(1, -1); + transform: scale(1, -1); +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-square:before, +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.min.css b/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.min.css new file mode 100644 index 0000000..3d920fc --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.1.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font-family:FontAwesome;font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:spin 2s infinite linear;-moz-animation:spin 2s infinite linear;-o-animation:spin 2s infinite linear;animation:spin 2s infinite linear}@-moz-keyframes spin{0%{-moz-transform:rotate(0deg)}100%{-moz-transform:rotate(359deg)}}@-webkit-keyframes spin{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg)}}@-o-keyframes spin{0%{-o-transform:rotate(0deg)}100%{-o-transform:rotate(359deg)}}@keyframes spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-moz-transform:scale(-1, 1);-ms-transform:scale(-1, 1);-o-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-moz-transform:scale(1, -1);-ms-transform:scale(1, -1);-o-transform:scale(1, -1);transform:scale(1, -1)}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-square:before,.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"} \ No newline at end of file diff --git a/saserverlibrary/src/main/resources/font-awesome/fonts/FontAwesome.otf b/saserverlibrary/src/main/resources/font-awesome/fonts/FontAwesome.otf new file mode 100644 index 0000000..3461e3f Binary files /dev/null and b/saserverlibrary/src/main/resources/font-awesome/fonts/FontAwesome.otf differ diff --git a/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.eot b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..6cfd566 Binary files /dev/null and b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.eot differ diff --git a/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.svg b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..a9f8469 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,504 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.ttf b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..5cd6cff Binary files /dev/null and b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.ttf differ diff --git a/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.woff b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..9eaecb3 Binary files /dev/null and b/saserverlibrary/src/main/resources/font-awesome/fonts/fontawesome-webfont.woff differ diff --git a/saserverlibrary/src/main/resources/font-awesome/less/bordered-pulled.less b/saserverlibrary/src/main/resources/font-awesome/less/bordered-pulled.less new file mode 100644 index 0000000..0c90eb5 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/bordered-pulled.less @@ -0,0 +1,16 @@ +// Bordered & Pulled +// ------------------------- + +.@{fa-css-prefix}-border { + padding: .2em .25em .15em; + border: solid .08em @fa-border-color; + border-radius: .1em; +} + +.pull-right { float: right; } +.pull-left { float: left; } + +.@{fa-css-prefix} { + &.pull-left { margin-right: .3em; } + &.pull-right { margin-left: .3em; } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/core.less b/saserverlibrary/src/main/resources/font-awesome/less/core.less new file mode 100644 index 0000000..6d223bc --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/core.less @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.@{fa-css-prefix} { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/fixed-width.less b/saserverlibrary/src/main/resources/font-awesome/less/fixed-width.less new file mode 100644 index 0000000..110289f --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/fixed-width.less @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.@{fa-css-prefix}-fw { + width: (18em / 14); + text-align: center; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/font-awesome.less b/saserverlibrary/src/main/resources/font-awesome/less/font-awesome.less new file mode 100644 index 0000000..50cbcac --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/font-awesome.less @@ -0,0 +1,17 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@import "variables.less"; +@import "mixins.less"; +@import "path.less"; +@import "core.less"; +@import "larger.less"; +@import "fixed-width.less"; +@import "list.less"; +@import "bordered-pulled.less"; +@import "spinning.less"; +@import "rotated-flipped.less"; +@import "stacked.less"; +@import "icons.less"; diff --git a/saserverlibrary/src/main/resources/font-awesome/less/icons.less b/saserverlibrary/src/main/resources/font-awesome/less/icons.less new file mode 100644 index 0000000..13d8c68 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/icons.less @@ -0,0 +1,506 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } +.@{fa-css-prefix}-music:before { content: @fa-var-music; } +.@{fa-css-prefix}-search:before { content: @fa-var-search; } +.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } +.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } +.@{fa-css-prefix}-star:before { content: @fa-var-star; } +.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } +.@{fa-css-prefix}-user:before { content: @fa-var-user; } +.@{fa-css-prefix}-film:before { content: @fa-var-film; } +.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } +.@{fa-css-prefix}-th:before { content: @fa-var-th; } +.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } +.@{fa-css-prefix}-check:before { content: @fa-var-check; } +.@{fa-css-prefix}-times:before { content: @fa-var-times; } +.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } +.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } +.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } +.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } +.@{fa-css-prefix}-gear:before, +.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } +.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } +.@{fa-css-prefix}-home:before { content: @fa-var-home; } +.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } +.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } +.@{fa-css-prefix}-road:before { content: @fa-var-road; } +.@{fa-css-prefix}-download:before { content: @fa-var-download; } +.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } +.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } +.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } +.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } +.@{fa-css-prefix}-rotate-right:before, +.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } +.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } +.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } +.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } +.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } +.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } +.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } +.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } +.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } +.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } +.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } +.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } +.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } +.@{fa-css-prefix}-book:before { content: @fa-var-book; } +.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } +.@{fa-css-prefix}-print:before { content: @fa-var-print; } +.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } +.@{fa-css-prefix}-font:before { content: @fa-var-font; } +.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } +.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } +.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } +.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } +.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } +.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } +.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } +.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } +.@{fa-css-prefix}-list:before { content: @fa-var-list; } +.@{fa-css-prefix}-dedent:before, +.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } +.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } +.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } +.@{fa-css-prefix}-photo:before, +.@{fa-css-prefix}-image:before, +.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } +.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } +.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } +.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } +.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } +.@{fa-css-prefix}-edit:before, +.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } +.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } +.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } +.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } +.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } +.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } +.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } +.@{fa-css-prefix}-play:before { content: @fa-var-play; } +.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } +.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } +.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } +.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } +.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } +.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } +.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } +.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } +.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } +.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } +.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } +.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } +.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } +.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } +.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } +.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } +.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } +.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } +.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } +.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } +.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } +.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } +.@{fa-css-prefix}-mail-forward:before, +.@{fa-css-prefix}-share:before { content: @fa-var-share; } +.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } +.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } +.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } +.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } +.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } +.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } +.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } +.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } +.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } +.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } +.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } +.@{fa-css-prefix}-warning:before, +.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } +.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } +.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } +.@{fa-css-prefix}-random:before { content: @fa-var-random; } +.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } +.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } +.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } +.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } +.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } +.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } +.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } +.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } +.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } +.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } +.@{fa-css-prefix}-bar-chart-o:before { content: @fa-var-bar-chart-o; } +.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } +.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } +.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } +.@{fa-css-prefix}-key:before { content: @fa-var-key; } +.@{fa-css-prefix}-gears:before, +.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } +.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } +.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } +.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } +.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } +.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } +.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } +.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } +.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } +.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } +.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } +.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } +.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } +.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } +.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } +.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } +.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } +.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } +.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } +.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } +.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } +.@{fa-css-prefix}-github:before { content: @fa-var-github; } +.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } +.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } +.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } +.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } +.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } +.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } +.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } +.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } +.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } +.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } +.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } +.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } +.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } +.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } +.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } +.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } +.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } +.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } +.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } +.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } +.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } +.@{fa-css-prefix}-group:before, +.@{fa-css-prefix}-users:before { content: @fa-var-users; } +.@{fa-css-prefix}-chain:before, +.@{fa-css-prefix}-link:before { content: @fa-var-link; } +.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } +.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } +.@{fa-css-prefix}-cut:before, +.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } +.@{fa-css-prefix}-copy:before, +.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } +.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } +.@{fa-css-prefix}-save:before, +.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } +.@{fa-css-prefix}-square:before { content: @fa-var-square; } +.@{fa-css-prefix}-navicon:before, +.@{fa-css-prefix}-reorder:before, +.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } +.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } +.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } +.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } +.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } +.@{fa-css-prefix}-table:before { content: @fa-var-table; } +.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } +.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } +.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } +.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } +.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } +.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } +.@{fa-css-prefix}-money:before { content: @fa-var-money; } +.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } +.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } +.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } +.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } +.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } +.@{fa-css-prefix}-unsorted:before, +.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } +.@{fa-css-prefix}-sort-down:before, +.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } +.@{fa-css-prefix}-sort-up:before, +.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } +.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } +.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } +.@{fa-css-prefix}-rotate-left:before, +.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } +.@{fa-css-prefix}-legal:before, +.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } +.@{fa-css-prefix}-dashboard:before, +.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } +.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } +.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } +.@{fa-css-prefix}-flash:before, +.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } +.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } +.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } +.@{fa-css-prefix}-paste:before, +.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } +.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } +.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } +.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } +.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } +.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } +.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } +.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } +.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } +.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } +.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } +.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } +.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } +.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } +.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } +.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } +.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } +.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } +.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } +.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } +.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } +.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } +.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } +.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } +.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } +.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } +.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } +.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } +.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } +.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } +.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } +.@{fa-css-prefix}-mobile-phone:before, +.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } +.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } +.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } +.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } +.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } +.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } +.@{fa-css-prefix}-mail-reply:before, +.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } +.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } +.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } +.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } +.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } +.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } +.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } +.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } +.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } +.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } +.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } +.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } +.@{fa-css-prefix}-code:before { content: @fa-var-code; } +.@{fa-css-prefix}-mail-reply-all:before, +.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } +.@{fa-css-prefix}-star-half-empty:before, +.@{fa-css-prefix}-star-half-full:before, +.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } +.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } +.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } +.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } +.@{fa-css-prefix}-unlink:before, +.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } +.@{fa-css-prefix}-question:before { content: @fa-var-question; } +.@{fa-css-prefix}-info:before { content: @fa-var-info; } +.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } +.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } +.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } +.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } +.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } +.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } +.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } +.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } +.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } +.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } +.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } +.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } +.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } +.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } +.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } +.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } +.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } +.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } +.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } +.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } +.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } +.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } +.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } +.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } +.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } +.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } +.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } +.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } +.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } +.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } +.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } +.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } +.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } +.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } +.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } +.@{fa-css-prefix}-toggle-down:before, +.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } +.@{fa-css-prefix}-toggle-up:before, +.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } +.@{fa-css-prefix}-toggle-right:before, +.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } +.@{fa-css-prefix}-euro:before, +.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } +.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } +.@{fa-css-prefix}-dollar:before, +.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } +.@{fa-css-prefix}-rupee:before, +.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } +.@{fa-css-prefix}-cny:before, +.@{fa-css-prefix}-rmb:before, +.@{fa-css-prefix}-yen:before, +.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } +.@{fa-css-prefix}-ruble:before, +.@{fa-css-prefix}-rouble:before, +.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } +.@{fa-css-prefix}-won:before, +.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } +.@{fa-css-prefix}-bitcoin:before, +.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } +.@{fa-css-prefix}-file:before { content: @fa-var-file; } +.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } +.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } +.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } +.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } +.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } +.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } +.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } +.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } +.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } +.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } +.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } +.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } +.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } +.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } +.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } +.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } +.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } +.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } +.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } +.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } +.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } +.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } +.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } +.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } +.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } +.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } +.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } +.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } +.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } +.@{fa-css-prefix}-android:before { content: @fa-var-android; } +.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } +.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } +.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } +.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } +.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } +.@{fa-css-prefix}-female:before { content: @fa-var-female; } +.@{fa-css-prefix}-male:before { content: @fa-var-male; } +.@{fa-css-prefix}-gittip:before { content: @fa-var-gittip; } +.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } +.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } +.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } +.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } +.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } +.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } +.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } +.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } +.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } +.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } +.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } +.@{fa-css-prefix}-toggle-left:before, +.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } +.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } +.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } +.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } +.@{fa-css-prefix}-turkish-lira:before, +.@{fa-css-prefix}-try:before { content: @fa-var-try; } +.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } +.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } +.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } +.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } +.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } +.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } +.@{fa-css-prefix}-institution:before, +.@{fa-css-prefix}-bank:before, +.@{fa-css-prefix}-university:before { content: @fa-var-university; } +.@{fa-css-prefix}-mortar-board:before, +.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } +.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } +.@{fa-css-prefix}-google:before { content: @fa-var-google; } +.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } +.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } +.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } +.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } +.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } +.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } +.@{fa-css-prefix}-pied-piper-square:before, +.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } +.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } +.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } +.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } +.@{fa-css-prefix}-language:before { content: @fa-var-language; } +.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } +.@{fa-css-prefix}-building:before { content: @fa-var-building; } +.@{fa-css-prefix}-child:before { content: @fa-var-child; } +.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } +.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } +.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } +.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } +.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } +.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } +.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } +.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } +.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } +.@{fa-css-prefix}-automobile:before, +.@{fa-css-prefix}-car:before { content: @fa-var-car; } +.@{fa-css-prefix}-cab:before, +.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } +.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } +.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } +.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } +.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } +.@{fa-css-prefix}-database:before { content: @fa-var-database; } +.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } +.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } +.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } +.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } +.@{fa-css-prefix}-file-photo-o:before, +.@{fa-css-prefix}-file-picture-o:before, +.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } +.@{fa-css-prefix}-file-zip-o:before, +.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } +.@{fa-css-prefix}-file-sound-o:before, +.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } +.@{fa-css-prefix}-file-movie-o:before, +.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } +.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } +.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } +.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } +.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } +.@{fa-css-prefix}-life-bouy:before, +.@{fa-css-prefix}-life-saver:before, +.@{fa-css-prefix}-support:before, +.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } +.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } +.@{fa-css-prefix}-ra:before, +.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } +.@{fa-css-prefix}-ge:before, +.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } +.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } +.@{fa-css-prefix}-git:before { content: @fa-var-git; } +.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } +.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } +.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } +.@{fa-css-prefix}-wechat:before, +.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } +.@{fa-css-prefix}-send:before, +.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } +.@{fa-css-prefix}-send-o:before, +.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } +.@{fa-css-prefix}-history:before { content: @fa-var-history; } +.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } +.@{fa-css-prefix}-header:before { content: @fa-var-header; } +.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } +.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } +.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } +.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } +.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } diff --git a/saserverlibrary/src/main/resources/font-awesome/less/larger.less b/saserverlibrary/src/main/resources/font-awesome/less/larger.less new file mode 100644 index 0000000..c9d6467 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/larger.less @@ -0,0 +1,13 @@ +// Icon Sizes +// ------------------------- + +/* makes the font 33% larger relative to the icon container */ +.@{fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -15%; +} +.@{fa-css-prefix}-2x { font-size: 2em; } +.@{fa-css-prefix}-3x { font-size: 3em; } +.@{fa-css-prefix}-4x { font-size: 4em; } +.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/saserverlibrary/src/main/resources/font-awesome/less/list.less b/saserverlibrary/src/main/resources/font-awesome/less/list.less new file mode 100644 index 0000000..eed9340 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/list.less @@ -0,0 +1,19 @@ +// List Icons +// ------------------------- + +.@{fa-css-prefix}-ul { + padding-left: 0; + margin-left: @fa-li-width; + list-style-type: none; + > li { position: relative; } +} +.@{fa-css-prefix}-li { + position: absolute; + left: -@fa-li-width; + width: @fa-li-width; + top: (2em / 14); + text-align: center; + &.@{fa-css-prefix}-lg { + left: -@fa-li-width + (4em / 14); + } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/mixins.less b/saserverlibrary/src/main/resources/font-awesome/less/mixins.less new file mode 100644 index 0000000..19e5a64 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/mixins.less @@ -0,0 +1,20 @@ +// Mixins +// -------------------------- + +.fa-icon-rotate(@degrees, @rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); + -webkit-transform: rotate(@degrees); + -moz-transform: rotate(@degrees); + -ms-transform: rotate(@degrees); + -o-transform: rotate(@degrees); + transform: rotate(@degrees); +} + +.fa-icon-flip(@horiz, @vert, @rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); + -webkit-transform: scale(@horiz, @vert); + -moz-transform: scale(@horiz, @vert); + -ms-transform: scale(@horiz, @vert); + -o-transform: scale(@horiz, @vert); + transform: scale(@horiz, @vert); +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/path.less b/saserverlibrary/src/main/resources/font-awesome/less/path.less new file mode 100644 index 0000000..d73bff8 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/path.less @@ -0,0 +1,14 @@ +/* FONT PATH + * -------------------------- */ + +@font-face { + font-family: 'FontAwesome'; + src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}')"; + src: ~"url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype')", + ~"url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff')", + ~"url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype')", + ~"url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg')"; +// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-weight: normal; + font-style: normal; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/rotated-flipped.less b/saserverlibrary/src/main/resources/font-awesome/less/rotated-flipped.less new file mode 100644 index 0000000..8fff3a6 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/rotated-flipped.less @@ -0,0 +1,9 @@ +// Rotated & Flipped Icons +// ------------------------- + +.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } +.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } +.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } + +.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } +.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } diff --git a/saserverlibrary/src/main/resources/font-awesome/less/spinning.less b/saserverlibrary/src/main/resources/font-awesome/less/spinning.less new file mode 100644 index 0000000..06b71ec --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/spinning.less @@ -0,0 +1,32 @@ +// Spinning Icons +// -------------------------- + +.@{fa-css-prefix}-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} + +@-moz-keyframes spin { + 0% { -moz-transform: rotate(0deg); } + 100% { -moz-transform: rotate(359deg); } +} +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(359deg); } +} +@-o-keyframes spin { + 0% { -o-transform: rotate(0deg); } + 100% { -o-transform: rotate(359deg); } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/less/stacked.less b/saserverlibrary/src/main/resources/font-awesome/less/stacked.less new file mode 100644 index 0000000..fc53fb0 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/stacked.less @@ -0,0 +1,20 @@ +// Stacked Icons +// ------------------------- + +.@{fa-css-prefix}-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.@{fa-css-prefix}-stack-1x { line-height: inherit; } +.@{fa-css-prefix}-stack-2x { font-size: 2em; } +.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/saserverlibrary/src/main/resources/font-awesome/less/variables.less b/saserverlibrary/src/main/resources/font-awesome/less/variables.less new file mode 100644 index 0000000..d7e8bd7 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/less/variables.less @@ -0,0 +1,515 @@ +// Variables +// -------------------------- + +@fa-font-path: "../fonts"; +//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.1.0/fonts"; // for referencing Bootstrap CDN font files directly +@fa-css-prefix: fa; +@fa-version: "4.1.0"; +@fa-border-color: #eee; +@fa-inverse: #fff; +@fa-li-width: (30em / 14); + +@fa-var-adjust: "\f042"; +@fa-var-adn: "\f170"; +@fa-var-align-center: "\f037"; +@fa-var-align-justify: "\f039"; +@fa-var-align-left: "\f036"; +@fa-var-align-right: "\f038"; +@fa-var-ambulance: "\f0f9"; +@fa-var-anchor: "\f13d"; +@fa-var-android: "\f17b"; +@fa-var-angle-double-down: "\f103"; +@fa-var-angle-double-left: "\f100"; +@fa-var-angle-double-right: "\f101"; +@fa-var-angle-double-up: "\f102"; +@fa-var-angle-down: "\f107"; +@fa-var-angle-left: "\f104"; +@fa-var-angle-right: "\f105"; +@fa-var-angle-up: "\f106"; +@fa-var-apple: "\f179"; +@fa-var-archive: "\f187"; +@fa-var-arrow-circle-down: "\f0ab"; +@fa-var-arrow-circle-left: "\f0a8"; +@fa-var-arrow-circle-o-down: "\f01a"; +@fa-var-arrow-circle-o-left: "\f190"; +@fa-var-arrow-circle-o-right: "\f18e"; +@fa-var-arrow-circle-o-up: "\f01b"; +@fa-var-arrow-circle-right: "\f0a9"; +@fa-var-arrow-circle-up: "\f0aa"; +@fa-var-arrow-down: "\f063"; +@fa-var-arrow-left: "\f060"; +@fa-var-arrow-right: "\f061"; +@fa-var-arrow-up: "\f062"; +@fa-var-arrows: "\f047"; +@fa-var-arrows-alt: "\f0b2"; +@fa-var-arrows-h: "\f07e"; +@fa-var-arrows-v: "\f07d"; +@fa-var-asterisk: "\f069"; +@fa-var-automobile: "\f1b9"; +@fa-var-backward: "\f04a"; +@fa-var-ban: "\f05e"; +@fa-var-bank: "\f19c"; +@fa-var-bar-chart-o: "\f080"; +@fa-var-barcode: "\f02a"; +@fa-var-bars: "\f0c9"; +@fa-var-beer: "\f0fc"; +@fa-var-behance: "\f1b4"; +@fa-var-behance-square: "\f1b5"; +@fa-var-bell: "\f0f3"; +@fa-var-bell-o: "\f0a2"; +@fa-var-bitbucket: "\f171"; +@fa-var-bitbucket-square: "\f172"; +@fa-var-bitcoin: "\f15a"; +@fa-var-bold: "\f032"; +@fa-var-bolt: "\f0e7"; +@fa-var-bomb: "\f1e2"; +@fa-var-book: "\f02d"; +@fa-var-bookmark: "\f02e"; +@fa-var-bookmark-o: "\f097"; +@fa-var-briefcase: "\f0b1"; +@fa-var-btc: "\f15a"; +@fa-var-bug: "\f188"; +@fa-var-building: "\f1ad"; +@fa-var-building-o: "\f0f7"; +@fa-var-bullhorn: "\f0a1"; +@fa-var-bullseye: "\f140"; +@fa-var-cab: "\f1ba"; +@fa-var-calendar: "\f073"; +@fa-var-calendar-o: "\f133"; +@fa-var-camera: "\f030"; +@fa-var-camera-retro: "\f083"; +@fa-var-car: "\f1b9"; +@fa-var-caret-down: "\f0d7"; +@fa-var-caret-left: "\f0d9"; +@fa-var-caret-right: "\f0da"; +@fa-var-caret-square-o-down: "\f150"; +@fa-var-caret-square-o-left: "\f191"; +@fa-var-caret-square-o-right: "\f152"; +@fa-var-caret-square-o-up: "\f151"; +@fa-var-caret-up: "\f0d8"; +@fa-var-certificate: "\f0a3"; +@fa-var-chain: "\f0c1"; +@fa-var-chain-broken: "\f127"; +@fa-var-check: "\f00c"; +@fa-var-check-circle: "\f058"; +@fa-var-check-circle-o: "\f05d"; +@fa-var-check-square: "\f14a"; +@fa-var-check-square-o: "\f046"; +@fa-var-chevron-circle-down: "\f13a"; +@fa-var-chevron-circle-left: "\f137"; +@fa-var-chevron-circle-right: "\f138"; +@fa-var-chevron-circle-up: "\f139"; +@fa-var-chevron-down: "\f078"; +@fa-var-chevron-left: "\f053"; +@fa-var-chevron-right: "\f054"; +@fa-var-chevron-up: "\f077"; +@fa-var-child: "\f1ae"; +@fa-var-circle: "\f111"; +@fa-var-circle-o: "\f10c"; +@fa-var-circle-o-notch: "\f1ce"; +@fa-var-circle-thin: "\f1db"; +@fa-var-clipboard: "\f0ea"; +@fa-var-clock-o: "\f017"; +@fa-var-cloud: "\f0c2"; +@fa-var-cloud-download: "\f0ed"; +@fa-var-cloud-upload: "\f0ee"; +@fa-var-cny: "\f157"; +@fa-var-code: "\f121"; +@fa-var-code-fork: "\f126"; +@fa-var-codepen: "\f1cb"; +@fa-var-coffee: "\f0f4"; +@fa-var-cog: "\f013"; +@fa-var-cogs: "\f085"; +@fa-var-columns: "\f0db"; +@fa-var-comment: "\f075"; +@fa-var-comment-o: "\f0e5"; +@fa-var-comments: "\f086"; +@fa-var-comments-o: "\f0e6"; +@fa-var-compass: "\f14e"; +@fa-var-compress: "\f066"; +@fa-var-copy: "\f0c5"; +@fa-var-credit-card: "\f09d"; +@fa-var-crop: "\f125"; +@fa-var-crosshairs: "\f05b"; +@fa-var-css3: "\f13c"; +@fa-var-cube: "\f1b2"; +@fa-var-cubes: "\f1b3"; +@fa-var-cut: "\f0c4"; +@fa-var-cutlery: "\f0f5"; +@fa-var-dashboard: "\f0e4"; +@fa-var-database: "\f1c0"; +@fa-var-dedent: "\f03b"; +@fa-var-delicious: "\f1a5"; +@fa-var-desktop: "\f108"; +@fa-var-deviantart: "\f1bd"; +@fa-var-digg: "\f1a6"; +@fa-var-dollar: "\f155"; +@fa-var-dot-circle-o: "\f192"; +@fa-var-download: "\f019"; +@fa-var-dribbble: "\f17d"; +@fa-var-dropbox: "\f16b"; +@fa-var-drupal: "\f1a9"; +@fa-var-edit: "\f044"; +@fa-var-eject: "\f052"; +@fa-var-ellipsis-h: "\f141"; +@fa-var-ellipsis-v: "\f142"; +@fa-var-empire: "\f1d1"; +@fa-var-envelope: "\f0e0"; +@fa-var-envelope-o: "\f003"; +@fa-var-envelope-square: "\f199"; +@fa-var-eraser: "\f12d"; +@fa-var-eur: "\f153"; +@fa-var-euro: "\f153"; +@fa-var-exchange: "\f0ec"; +@fa-var-exclamation: "\f12a"; +@fa-var-exclamation-circle: "\f06a"; +@fa-var-exclamation-triangle: "\f071"; +@fa-var-expand: "\f065"; +@fa-var-external-link: "\f08e"; +@fa-var-external-link-square: "\f14c"; +@fa-var-eye: "\f06e"; +@fa-var-eye-slash: "\f070"; +@fa-var-facebook: "\f09a"; +@fa-var-facebook-square: "\f082"; +@fa-var-fast-backward: "\f049"; +@fa-var-fast-forward: "\f050"; +@fa-var-fax: "\f1ac"; +@fa-var-female: "\f182"; +@fa-var-fighter-jet: "\f0fb"; +@fa-var-file: "\f15b"; +@fa-var-file-archive-o: "\f1c6"; +@fa-var-file-audio-o: "\f1c7"; +@fa-var-file-code-o: "\f1c9"; +@fa-var-file-excel-o: "\f1c3"; +@fa-var-file-image-o: "\f1c5"; +@fa-var-file-movie-o: "\f1c8"; +@fa-var-file-o: "\f016"; +@fa-var-file-pdf-o: "\f1c1"; +@fa-var-file-photo-o: "\f1c5"; +@fa-var-file-picture-o: "\f1c5"; +@fa-var-file-powerpoint-o: "\f1c4"; +@fa-var-file-sound-o: "\f1c7"; +@fa-var-file-text: "\f15c"; +@fa-var-file-text-o: "\f0f6"; +@fa-var-file-video-o: "\f1c8"; +@fa-var-file-word-o: "\f1c2"; +@fa-var-file-zip-o: "\f1c6"; +@fa-var-files-o: "\f0c5"; +@fa-var-film: "\f008"; +@fa-var-filter: "\f0b0"; +@fa-var-fire: "\f06d"; +@fa-var-fire-extinguisher: "\f134"; +@fa-var-flag: "\f024"; +@fa-var-flag-checkered: "\f11e"; +@fa-var-flag-o: "\f11d"; +@fa-var-flash: "\f0e7"; +@fa-var-flask: "\f0c3"; +@fa-var-flickr: "\f16e"; +@fa-var-floppy-o: "\f0c7"; +@fa-var-folder: "\f07b"; +@fa-var-folder-o: "\f114"; +@fa-var-folder-open: "\f07c"; +@fa-var-folder-open-o: "\f115"; +@fa-var-font: "\f031"; +@fa-var-forward: "\f04e"; +@fa-var-foursquare: "\f180"; +@fa-var-frown-o: "\f119"; +@fa-var-gamepad: "\f11b"; +@fa-var-gavel: "\f0e3"; +@fa-var-gbp: "\f154"; +@fa-var-ge: "\f1d1"; +@fa-var-gear: "\f013"; +@fa-var-gears: "\f085"; +@fa-var-gift: "\f06b"; +@fa-var-git: "\f1d3"; +@fa-var-git-square: "\f1d2"; +@fa-var-github: "\f09b"; +@fa-var-github-alt: "\f113"; +@fa-var-github-square: "\f092"; +@fa-var-gittip: "\f184"; +@fa-var-glass: "\f000"; +@fa-var-globe: "\f0ac"; +@fa-var-google: "\f1a0"; +@fa-var-google-plus: "\f0d5"; +@fa-var-google-plus-square: "\f0d4"; +@fa-var-graduation-cap: "\f19d"; +@fa-var-group: "\f0c0"; +@fa-var-h-square: "\f0fd"; +@fa-var-hacker-news: "\f1d4"; +@fa-var-hand-o-down: "\f0a7"; +@fa-var-hand-o-left: "\f0a5"; +@fa-var-hand-o-right: "\f0a4"; +@fa-var-hand-o-up: "\f0a6"; +@fa-var-hdd-o: "\f0a0"; +@fa-var-header: "\f1dc"; +@fa-var-headphones: "\f025"; +@fa-var-heart: "\f004"; +@fa-var-heart-o: "\f08a"; +@fa-var-history: "\f1da"; +@fa-var-home: "\f015"; +@fa-var-hospital-o: "\f0f8"; +@fa-var-html5: "\f13b"; +@fa-var-image: "\f03e"; +@fa-var-inbox: "\f01c"; +@fa-var-indent: "\f03c"; +@fa-var-info: "\f129"; +@fa-var-info-circle: "\f05a"; +@fa-var-inr: "\f156"; +@fa-var-instagram: "\f16d"; +@fa-var-institution: "\f19c"; +@fa-var-italic: "\f033"; +@fa-var-joomla: "\f1aa"; +@fa-var-jpy: "\f157"; +@fa-var-jsfiddle: "\f1cc"; +@fa-var-key: "\f084"; +@fa-var-keyboard-o: "\f11c"; +@fa-var-krw: "\f159"; +@fa-var-language: "\f1ab"; +@fa-var-laptop: "\f109"; +@fa-var-leaf: "\f06c"; +@fa-var-legal: "\f0e3"; +@fa-var-lemon-o: "\f094"; +@fa-var-level-down: "\f149"; +@fa-var-level-up: "\f148"; +@fa-var-life-bouy: "\f1cd"; +@fa-var-life-ring: "\f1cd"; +@fa-var-life-saver: "\f1cd"; +@fa-var-lightbulb-o: "\f0eb"; +@fa-var-link: "\f0c1"; +@fa-var-linkedin: "\f0e1"; +@fa-var-linkedin-square: "\f08c"; +@fa-var-linux: "\f17c"; +@fa-var-list: "\f03a"; +@fa-var-list-alt: "\f022"; +@fa-var-list-ol: "\f0cb"; +@fa-var-list-ul: "\f0ca"; +@fa-var-location-arrow: "\f124"; +@fa-var-lock: "\f023"; +@fa-var-long-arrow-down: "\f175"; +@fa-var-long-arrow-left: "\f177"; +@fa-var-long-arrow-right: "\f178"; +@fa-var-long-arrow-up: "\f176"; +@fa-var-magic: "\f0d0"; +@fa-var-magnet: "\f076"; +@fa-var-mail-forward: "\f064"; +@fa-var-mail-reply: "\f112"; +@fa-var-mail-reply-all: "\f122"; +@fa-var-male: "\f183"; +@fa-var-map-marker: "\f041"; +@fa-var-maxcdn: "\f136"; +@fa-var-medkit: "\f0fa"; +@fa-var-meh-o: "\f11a"; +@fa-var-microphone: "\f130"; +@fa-var-microphone-slash: "\f131"; +@fa-var-minus: "\f068"; +@fa-var-minus-circle: "\f056"; +@fa-var-minus-square: "\f146"; +@fa-var-minus-square-o: "\f147"; +@fa-var-mobile: "\f10b"; +@fa-var-mobile-phone: "\f10b"; +@fa-var-money: "\f0d6"; +@fa-var-moon-o: "\f186"; +@fa-var-mortar-board: "\f19d"; +@fa-var-music: "\f001"; +@fa-var-navicon: "\f0c9"; +@fa-var-openid: "\f19b"; +@fa-var-outdent: "\f03b"; +@fa-var-pagelines: "\f18c"; +@fa-var-paper-plane: "\f1d8"; +@fa-var-paper-plane-o: "\f1d9"; +@fa-var-paperclip: "\f0c6"; +@fa-var-paragraph: "\f1dd"; +@fa-var-paste: "\f0ea"; +@fa-var-pause: "\f04c"; +@fa-var-paw: "\f1b0"; +@fa-var-pencil: "\f040"; +@fa-var-pencil-square: "\f14b"; +@fa-var-pencil-square-o: "\f044"; +@fa-var-phone: "\f095"; +@fa-var-phone-square: "\f098"; +@fa-var-photo: "\f03e"; +@fa-var-picture-o: "\f03e"; +@fa-var-pied-piper: "\f1a7"; +@fa-var-pied-piper-alt: "\f1a8"; +@fa-var-pied-piper-square: "\f1a7"; +@fa-var-pinterest: "\f0d2"; +@fa-var-pinterest-square: "\f0d3"; +@fa-var-plane: "\f072"; +@fa-var-play: "\f04b"; +@fa-var-play-circle: "\f144"; +@fa-var-play-circle-o: "\f01d"; +@fa-var-plus: "\f067"; +@fa-var-plus-circle: "\f055"; +@fa-var-plus-square: "\f0fe"; +@fa-var-plus-square-o: "\f196"; +@fa-var-power-off: "\f011"; +@fa-var-print: "\f02f"; +@fa-var-puzzle-piece: "\f12e"; +@fa-var-qq: "\f1d6"; +@fa-var-qrcode: "\f029"; +@fa-var-question: "\f128"; +@fa-var-question-circle: "\f059"; +@fa-var-quote-left: "\f10d"; +@fa-var-quote-right: "\f10e"; +@fa-var-ra: "\f1d0"; +@fa-var-random: "\f074"; +@fa-var-rebel: "\f1d0"; +@fa-var-recycle: "\f1b8"; +@fa-var-reddit: "\f1a1"; +@fa-var-reddit-square: "\f1a2"; +@fa-var-refresh: "\f021"; +@fa-var-renren: "\f18b"; +@fa-var-reorder: "\f0c9"; +@fa-var-repeat: "\f01e"; +@fa-var-reply: "\f112"; +@fa-var-reply-all: "\f122"; +@fa-var-retweet: "\f079"; +@fa-var-rmb: "\f157"; +@fa-var-road: "\f018"; +@fa-var-rocket: "\f135"; +@fa-var-rotate-left: "\f0e2"; +@fa-var-rotate-right: "\f01e"; +@fa-var-rouble: "\f158"; +@fa-var-rss: "\f09e"; +@fa-var-rss-square: "\f143"; +@fa-var-rub: "\f158"; +@fa-var-ruble: "\f158"; +@fa-var-rupee: "\f156"; +@fa-var-save: "\f0c7"; +@fa-var-scissors: "\f0c4"; +@fa-var-search: "\f002"; +@fa-var-search-minus: "\f010"; +@fa-var-search-plus: "\f00e"; +@fa-var-send: "\f1d8"; +@fa-var-send-o: "\f1d9"; +@fa-var-share: "\f064"; +@fa-var-share-alt: "\f1e0"; +@fa-var-share-alt-square: "\f1e1"; +@fa-var-share-square: "\f14d"; +@fa-var-share-square-o: "\f045"; +@fa-var-shield: "\f132"; +@fa-var-shopping-cart: "\f07a"; +@fa-var-sign-in: "\f090"; +@fa-var-sign-out: "\f08b"; +@fa-var-signal: "\f012"; +@fa-var-sitemap: "\f0e8"; +@fa-var-skype: "\f17e"; +@fa-var-slack: "\f198"; +@fa-var-sliders: "\f1de"; +@fa-var-smile-o: "\f118"; +@fa-var-sort: "\f0dc"; +@fa-var-sort-alpha-asc: "\f15d"; +@fa-var-sort-alpha-desc: "\f15e"; +@fa-var-sort-amount-asc: "\f160"; +@fa-var-sort-amount-desc: "\f161"; +@fa-var-sort-asc: "\f0de"; +@fa-var-sort-desc: "\f0dd"; +@fa-var-sort-down: "\f0dd"; +@fa-var-sort-numeric-asc: "\f162"; +@fa-var-sort-numeric-desc: "\f163"; +@fa-var-sort-up: "\f0de"; +@fa-var-soundcloud: "\f1be"; +@fa-var-space-shuttle: "\f197"; +@fa-var-spinner: "\f110"; +@fa-var-spoon: "\f1b1"; +@fa-var-spotify: "\f1bc"; +@fa-var-square: "\f0c8"; +@fa-var-square-o: "\f096"; +@fa-var-stack-exchange: "\f18d"; +@fa-var-stack-overflow: "\f16c"; +@fa-var-star: "\f005"; +@fa-var-star-half: "\f089"; +@fa-var-star-half-empty: "\f123"; +@fa-var-star-half-full: "\f123"; +@fa-var-star-half-o: "\f123"; +@fa-var-star-o: "\f006"; +@fa-var-steam: "\f1b6"; +@fa-var-steam-square: "\f1b7"; +@fa-var-step-backward: "\f048"; +@fa-var-step-forward: "\f051"; +@fa-var-stethoscope: "\f0f1"; +@fa-var-stop: "\f04d"; +@fa-var-strikethrough: "\f0cc"; +@fa-var-stumbleupon: "\f1a4"; +@fa-var-stumbleupon-circle: "\f1a3"; +@fa-var-subscript: "\f12c"; +@fa-var-suitcase: "\f0f2"; +@fa-var-sun-o: "\f185"; +@fa-var-superscript: "\f12b"; +@fa-var-support: "\f1cd"; +@fa-var-table: "\f0ce"; +@fa-var-tablet: "\f10a"; +@fa-var-tachometer: "\f0e4"; +@fa-var-tag: "\f02b"; +@fa-var-tags: "\f02c"; +@fa-var-tasks: "\f0ae"; +@fa-var-taxi: "\f1ba"; +@fa-var-tencent-weibo: "\f1d5"; +@fa-var-terminal: "\f120"; +@fa-var-text-height: "\f034"; +@fa-var-text-width: "\f035"; +@fa-var-th: "\f00a"; +@fa-var-th-large: "\f009"; +@fa-var-th-list: "\f00b"; +@fa-var-thumb-tack: "\f08d"; +@fa-var-thumbs-down: "\f165"; +@fa-var-thumbs-o-down: "\f088"; +@fa-var-thumbs-o-up: "\f087"; +@fa-var-thumbs-up: "\f164"; +@fa-var-ticket: "\f145"; +@fa-var-times: "\f00d"; +@fa-var-times-circle: "\f057"; +@fa-var-times-circle-o: "\f05c"; +@fa-var-tint: "\f043"; +@fa-var-toggle-down: "\f150"; +@fa-var-toggle-left: "\f191"; +@fa-var-toggle-right: "\f152"; +@fa-var-toggle-up: "\f151"; +@fa-var-trash-o: "\f014"; +@fa-var-tree: "\f1bb"; +@fa-var-trello: "\f181"; +@fa-var-trophy: "\f091"; +@fa-var-truck: "\f0d1"; +@fa-var-try: "\f195"; +@fa-var-tumblr: "\f173"; +@fa-var-tumblr-square: "\f174"; +@fa-var-turkish-lira: "\f195"; +@fa-var-twitter: "\f099"; +@fa-var-twitter-square: "\f081"; +@fa-var-umbrella: "\f0e9"; +@fa-var-underline: "\f0cd"; +@fa-var-undo: "\f0e2"; +@fa-var-university: "\f19c"; +@fa-var-unlink: "\f127"; +@fa-var-unlock: "\f09c"; +@fa-var-unlock-alt: "\f13e"; +@fa-var-unsorted: "\f0dc"; +@fa-var-upload: "\f093"; +@fa-var-usd: "\f155"; +@fa-var-user: "\f007"; +@fa-var-user-md: "\f0f0"; +@fa-var-users: "\f0c0"; +@fa-var-video-camera: "\f03d"; +@fa-var-vimeo-square: "\f194"; +@fa-var-vine: "\f1ca"; +@fa-var-vk: "\f189"; +@fa-var-volume-down: "\f027"; +@fa-var-volume-off: "\f026"; +@fa-var-volume-up: "\f028"; +@fa-var-warning: "\f071"; +@fa-var-wechat: "\f1d7"; +@fa-var-weibo: "\f18a"; +@fa-var-weixin: "\f1d7"; +@fa-var-wheelchair: "\f193"; +@fa-var-windows: "\f17a"; +@fa-var-won: "\f159"; +@fa-var-wordpress: "\f19a"; +@fa-var-wrench: "\f0ad"; +@fa-var-xing: "\f168"; +@fa-var-xing-square: "\f169"; +@fa-var-yahoo: "\f19e"; +@fa-var-yen: "\f157"; +@fa-var-youtube: "\f167"; +@fa-var-youtube-play: "\f16a"; +@fa-var-youtube-square: "\f166"; + diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_bordered-pulled.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_bordered-pulled.scss new file mode 100644 index 0000000..9d3fdf3 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_bordered-pulled.scss @@ -0,0 +1,16 @@ +// Bordered & Pulled +// ------------------------- + +.#{$fa-css-prefix}-border { + padding: .2em .25em .15em; + border: solid .08em $fa-border-color; + border-radius: .1em; +} + +.pull-right { float: right; } +.pull-left { float: left; } + +.#{$fa-css-prefix} { + &.pull-left { margin-right: .3em; } + &.pull-right { margin-left: .3em; } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_core.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_core.scss new file mode 100644 index 0000000..861ccd9 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_core.scss @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.#{$fa-css-prefix} { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_fixed-width.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_fixed-width.scss new file mode 100644 index 0000000..b221c98 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_fixed-width.scss @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.#{$fa-css-prefix}-fw { + width: (18em / 14); + text-align: center; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_icons.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_icons.scss new file mode 100644 index 0000000..efb4435 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_icons.scss @@ -0,0 +1,506 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } +.#{$fa-css-prefix}-music:before { content: $fa-var-music; } +.#{$fa-css-prefix}-search:before { content: $fa-var-search; } +.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } +.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } +.#{$fa-css-prefix}-star:before { content: $fa-var-star; } +.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } +.#{$fa-css-prefix}-user:before { content: $fa-var-user; } +.#{$fa-css-prefix}-film:before { content: $fa-var-film; } +.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } +.#{$fa-css-prefix}-th:before { content: $fa-var-th; } +.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } +.#{$fa-css-prefix}-check:before { content: $fa-var-check; } +.#{$fa-css-prefix}-times:before { content: $fa-var-times; } +.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } +.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } +.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } +.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } +.#{$fa-css-prefix}-gear:before, +.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } +.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } +.#{$fa-css-prefix}-home:before { content: $fa-var-home; } +.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } +.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } +.#{$fa-css-prefix}-road:before { content: $fa-var-road; } +.#{$fa-css-prefix}-download:before { content: $fa-var-download; } +.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } +.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } +.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } +.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } +.#{$fa-css-prefix}-rotate-right:before, +.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } +.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } +.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } +.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } +.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } +.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } +.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } +.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } +.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } +.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } +.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } +.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } +.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } +.#{$fa-css-prefix}-book:before { content: $fa-var-book; } +.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } +.#{$fa-css-prefix}-print:before { content: $fa-var-print; } +.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } +.#{$fa-css-prefix}-font:before { content: $fa-var-font; } +.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } +.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } +.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } +.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } +.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } +.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } +.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } +.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } +.#{$fa-css-prefix}-list:before { content: $fa-var-list; } +.#{$fa-css-prefix}-dedent:before, +.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } +.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } +.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } +.#{$fa-css-prefix}-photo:before, +.#{$fa-css-prefix}-image:before, +.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } +.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } +.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } +.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } +.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } +.#{$fa-css-prefix}-edit:before, +.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } +.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } +.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } +.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } +.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } +.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } +.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } +.#{$fa-css-prefix}-play:before { content: $fa-var-play; } +.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } +.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } +.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } +.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } +.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } +.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } +.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } +.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } +.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } +.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } +.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } +.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } +.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } +.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } +.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } +.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } +.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } +.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } +.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } +.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } +.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } +.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } +.#{$fa-css-prefix}-mail-forward:before, +.#{$fa-css-prefix}-share:before { content: $fa-var-share; } +.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } +.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } +.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } +.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } +.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } +.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } +.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } +.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } +.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } +.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } +.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } +.#{$fa-css-prefix}-warning:before, +.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } +.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } +.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } +.#{$fa-css-prefix}-random:before { content: $fa-var-random; } +.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } +.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } +.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } +.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } +.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } +.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } +.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } +.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } +.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } +.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } +.#{$fa-css-prefix}-bar-chart-o:before { content: $fa-var-bar-chart-o; } +.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } +.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } +.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } +.#{$fa-css-prefix}-key:before { content: $fa-var-key; } +.#{$fa-css-prefix}-gears:before, +.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } +.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } +.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } +.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } +.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } +.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } +.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } +.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } +.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } +.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } +.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } +.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } +.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } +.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } +.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } +.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } +.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } +.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } +.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } +.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } +.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } +.#{$fa-css-prefix}-github:before { content: $fa-var-github; } +.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } +.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } +.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } +.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } +.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } +.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } +.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } +.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } +.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } +.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } +.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } +.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } +.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } +.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } +.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } +.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } +.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } +.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } +.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } +.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } +.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } +.#{$fa-css-prefix}-group:before, +.#{$fa-css-prefix}-users:before { content: $fa-var-users; } +.#{$fa-css-prefix}-chain:before, +.#{$fa-css-prefix}-link:before { content: $fa-var-link; } +.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } +.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } +.#{$fa-css-prefix}-cut:before, +.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } +.#{$fa-css-prefix}-copy:before, +.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } +.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } +.#{$fa-css-prefix}-save:before, +.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } +.#{$fa-css-prefix}-square:before { content: $fa-var-square; } +.#{$fa-css-prefix}-navicon:before, +.#{$fa-css-prefix}-reorder:before, +.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } +.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } +.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } +.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } +.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } +.#{$fa-css-prefix}-table:before { content: $fa-var-table; } +.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } +.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } +.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } +.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } +.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } +.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } +.#{$fa-css-prefix}-money:before { content: $fa-var-money; } +.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } +.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } +.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } +.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } +.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } +.#{$fa-css-prefix}-unsorted:before, +.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } +.#{$fa-css-prefix}-sort-down:before, +.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } +.#{$fa-css-prefix}-sort-up:before, +.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } +.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } +.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } +.#{$fa-css-prefix}-rotate-left:before, +.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } +.#{$fa-css-prefix}-legal:before, +.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } +.#{$fa-css-prefix}-dashboard:before, +.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } +.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } +.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } +.#{$fa-css-prefix}-flash:before, +.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } +.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } +.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } +.#{$fa-css-prefix}-paste:before, +.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } +.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } +.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } +.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } +.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } +.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } +.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } +.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } +.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } +.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } +.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } +.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } +.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } +.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } +.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } +.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } +.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } +.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } +.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } +.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } +.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } +.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } +.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } +.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } +.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } +.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } +.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } +.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } +.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } +.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } +.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } +.#{$fa-css-prefix}-mobile-phone:before, +.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } +.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } +.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } +.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } +.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } +.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } +.#{$fa-css-prefix}-mail-reply:before, +.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } +.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } +.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } +.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } +.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } +.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } +.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } +.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } +.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } +.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } +.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } +.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } +.#{$fa-css-prefix}-code:before { content: $fa-var-code; } +.#{$fa-css-prefix}-mail-reply-all:before, +.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } +.#{$fa-css-prefix}-star-half-empty:before, +.#{$fa-css-prefix}-star-half-full:before, +.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } +.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } +.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } +.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } +.#{$fa-css-prefix}-unlink:before, +.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } +.#{$fa-css-prefix}-question:before { content: $fa-var-question; } +.#{$fa-css-prefix}-info:before { content: $fa-var-info; } +.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } +.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } +.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } +.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } +.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } +.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } +.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } +.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } +.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } +.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } +.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } +.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } +.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } +.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } +.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } +.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } +.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } +.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } +.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } +.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } +.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } +.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } +.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } +.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } +.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } +.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } +.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } +.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } +.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } +.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } +.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } +.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } +.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } +.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } +.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } +.#{$fa-css-prefix}-toggle-down:before, +.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } +.#{$fa-css-prefix}-toggle-up:before, +.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } +.#{$fa-css-prefix}-toggle-right:before, +.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } +.#{$fa-css-prefix}-euro:before, +.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } +.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } +.#{$fa-css-prefix}-dollar:before, +.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } +.#{$fa-css-prefix}-rupee:before, +.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } +.#{$fa-css-prefix}-cny:before, +.#{$fa-css-prefix}-rmb:before, +.#{$fa-css-prefix}-yen:before, +.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } +.#{$fa-css-prefix}-ruble:before, +.#{$fa-css-prefix}-rouble:before, +.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } +.#{$fa-css-prefix}-won:before, +.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } +.#{$fa-css-prefix}-bitcoin:before, +.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } +.#{$fa-css-prefix}-file:before { content: $fa-var-file; } +.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } +.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } +.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } +.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } +.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } +.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } +.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } +.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } +.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } +.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } +.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } +.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } +.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } +.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } +.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } +.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } +.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } +.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } +.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } +.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } +.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } +.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } +.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } +.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } +.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } +.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } +.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } +.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } +.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } +.#{$fa-css-prefix}-android:before { content: $fa-var-android; } +.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } +.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } +.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } +.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } +.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } +.#{$fa-css-prefix}-female:before { content: $fa-var-female; } +.#{$fa-css-prefix}-male:before { content: $fa-var-male; } +.#{$fa-css-prefix}-gittip:before { content: $fa-var-gittip; } +.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } +.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } +.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } +.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } +.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } +.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } +.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } +.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } +.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } +.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } +.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } +.#{$fa-css-prefix}-toggle-left:before, +.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } +.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } +.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } +.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } +.#{$fa-css-prefix}-turkish-lira:before, +.#{$fa-css-prefix}-try:before { content: $fa-var-try; } +.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } +.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } +.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } +.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } +.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } +.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } +.#{$fa-css-prefix}-institution:before, +.#{$fa-css-prefix}-bank:before, +.#{$fa-css-prefix}-university:before { content: $fa-var-university; } +.#{$fa-css-prefix}-mortar-board:before, +.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } +.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } +.#{$fa-css-prefix}-google:before { content: $fa-var-google; } +.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } +.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } +.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } +.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } +.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } +.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } +.#{$fa-css-prefix}-pied-piper-square:before, +.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } +.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } +.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } +.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } +.#{$fa-css-prefix}-language:before { content: $fa-var-language; } +.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } +.#{$fa-css-prefix}-building:before { content: $fa-var-building; } +.#{$fa-css-prefix}-child:before { content: $fa-var-child; } +.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } +.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } +.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } +.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } +.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } +.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } +.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } +.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } +.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } +.#{$fa-css-prefix}-automobile:before, +.#{$fa-css-prefix}-car:before { content: $fa-var-car; } +.#{$fa-css-prefix}-cab:before, +.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } +.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } +.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } +.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } +.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } +.#{$fa-css-prefix}-database:before { content: $fa-var-database; } +.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } +.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } +.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } +.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } +.#{$fa-css-prefix}-file-photo-o:before, +.#{$fa-css-prefix}-file-picture-o:before, +.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } +.#{$fa-css-prefix}-file-zip-o:before, +.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } +.#{$fa-css-prefix}-file-sound-o:before, +.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } +.#{$fa-css-prefix}-file-movie-o:before, +.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } +.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } +.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } +.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } +.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } +.#{$fa-css-prefix}-life-bouy:before, +.#{$fa-css-prefix}-life-saver:before, +.#{$fa-css-prefix}-support:before, +.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } +.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } +.#{$fa-css-prefix}-ra:before, +.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } +.#{$fa-css-prefix}-ge:before, +.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } +.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } +.#{$fa-css-prefix}-git:before { content: $fa-var-git; } +.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } +.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } +.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } +.#{$fa-css-prefix}-wechat:before, +.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } +.#{$fa-css-prefix}-send:before, +.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } +.#{$fa-css-prefix}-send-o:before, +.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } +.#{$fa-css-prefix}-history:before { content: $fa-var-history; } +.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } +.#{$fa-css-prefix}-header:before { content: $fa-var-header; } +.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } +.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } +.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } +.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } +.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_larger.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_larger.scss new file mode 100644 index 0000000..41e9a81 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_larger.scss @@ -0,0 +1,13 @@ +// Icon Sizes +// ------------------------- + +/* makes the font 33% larger relative to the icon container */ +.#{$fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -15%; +} +.#{$fa-css-prefix}-2x { font-size: 2em; } +.#{$fa-css-prefix}-3x { font-size: 3em; } +.#{$fa-css-prefix}-4x { font-size: 4em; } +.#{$fa-css-prefix}-5x { font-size: 5em; } diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_list.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_list.scss new file mode 100644 index 0000000..7d1e4d5 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_list.scss @@ -0,0 +1,19 @@ +// List Icons +// ------------------------- + +.#{$fa-css-prefix}-ul { + padding-left: 0; + margin-left: $fa-li-width; + list-style-type: none; + > li { position: relative; } +} +.#{$fa-css-prefix}-li { + position: absolute; + left: -$fa-li-width; + width: $fa-li-width; + top: (2em / 14); + text-align: center; + &.#{$fa-css-prefix}-lg { + left: -$fa-li-width + (4em / 14); + } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_mixins.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_mixins.scss new file mode 100644 index 0000000..3354e69 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_mixins.scss @@ -0,0 +1,20 @@ +// Mixins +// -------------------------- + +@mixin fa-icon-rotate($degrees, $rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); + -webkit-transform: rotate($degrees); + -moz-transform: rotate($degrees); + -ms-transform: rotate($degrees); + -o-transform: rotate($degrees); + transform: rotate($degrees); +} + +@mixin fa-icon-flip($horiz, $vert, $rotation) { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); + -webkit-transform: scale($horiz, $vert); + -moz-transform: scale($horiz, $vert); + -ms-transform: scale($horiz, $vert); + -o-transform: scale($horiz, $vert); + transform: scale($horiz, $vert); +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_path.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_path.scss new file mode 100644 index 0000000..fd21c35 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_path.scss @@ -0,0 +1,14 @@ +/* FONT PATH + * -------------------------- */ + +@font-face { + font-family: 'FontAwesome'; + src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); + src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), + url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), + url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), + url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); + //src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-weight: normal; + font-style: normal; +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_rotated-flipped.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_rotated-flipped.scss new file mode 100644 index 0000000..343fa55 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_rotated-flipped.scss @@ -0,0 +1,9 @@ +// Rotated & Flipped Icons +// ------------------------- + +.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } +.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } +.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } + +.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } +.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_spinning.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_spinning.scss new file mode 100644 index 0000000..c378744 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_spinning.scss @@ -0,0 +1,32 @@ +// Spinning Icons +// -------------------------- + +.#{$fa-css-prefix}-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} + +@-moz-keyframes spin { + 0% { -moz-transform: rotate(0deg); } + 100% { -moz-transform: rotate(359deg); } +} +@-webkit-keyframes spin { + 0% { -webkit-transform: rotate(0deg); } + 100% { -webkit-transform: rotate(359deg); } +} +@-o-keyframes spin { + 0% { -o-transform: rotate(0deg); } + 100% { -o-transform: rotate(359deg); } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_stacked.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_stacked.scss new file mode 100644 index 0000000..aef7403 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_stacked.scss @@ -0,0 +1,20 @@ +// Stacked Icons +// ------------------------- + +.#{$fa-css-prefix}-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.#{$fa-css-prefix}-stack-1x { line-height: inherit; } +.#{$fa-css-prefix}-stack-2x { font-size: 2em; } +.#{$fa-css-prefix}-inverse { color: $fa-inverse; } diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/_variables.scss b/saserverlibrary/src/main/resources/font-awesome/scss/_variables.scss new file mode 100644 index 0000000..ac2b505 --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/_variables.scss @@ -0,0 +1,515 @@ +// Variables +// -------------------------- + +$fa-font-path: "../fonts" !default; +//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.1.0/fonts" !default; // for referencing Bootstrap CDN font files directly +$fa-css-prefix: fa !default; +$fa-version: "4.1.0" !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: (30em / 14) !default; + +$fa-var-adjust: "\f042"; +$fa-var-adn: "\f170"; +$fa-var-align-center: "\f037"; +$fa-var-align-justify: "\f039"; +$fa-var-align-left: "\f036"; +$fa-var-align-right: "\f038"; +$fa-var-ambulance: "\f0f9"; +$fa-var-anchor: "\f13d"; +$fa-var-android: "\f17b"; +$fa-var-angle-double-down: "\f103"; +$fa-var-angle-double-left: "\f100"; +$fa-var-angle-double-right: "\f101"; +$fa-var-angle-double-up: "\f102"; +$fa-var-angle-down: "\f107"; +$fa-var-angle-left: "\f104"; +$fa-var-angle-right: "\f105"; +$fa-var-angle-up: "\f106"; +$fa-var-apple: "\f179"; +$fa-var-archive: "\f187"; +$fa-var-arrow-circle-down: "\f0ab"; +$fa-var-arrow-circle-left: "\f0a8"; +$fa-var-arrow-circle-o-down: "\f01a"; +$fa-var-arrow-circle-o-left: "\f190"; +$fa-var-arrow-circle-o-right: "\f18e"; +$fa-var-arrow-circle-o-up: "\f01b"; +$fa-var-arrow-circle-right: "\f0a9"; +$fa-var-arrow-circle-up: "\f0aa"; +$fa-var-arrow-down: "\f063"; +$fa-var-arrow-left: "\f060"; +$fa-var-arrow-right: "\f061"; +$fa-var-arrow-up: "\f062"; +$fa-var-arrows: "\f047"; +$fa-var-arrows-alt: "\f0b2"; +$fa-var-arrows-h: "\f07e"; +$fa-var-arrows-v: "\f07d"; +$fa-var-asterisk: "\f069"; +$fa-var-automobile: "\f1b9"; +$fa-var-backward: "\f04a"; +$fa-var-ban: "\f05e"; +$fa-var-bank: "\f19c"; +$fa-var-bar-chart-o: "\f080"; +$fa-var-barcode: "\f02a"; +$fa-var-bars: "\f0c9"; +$fa-var-beer: "\f0fc"; +$fa-var-behance: "\f1b4"; +$fa-var-behance-square: "\f1b5"; +$fa-var-bell: "\f0f3"; +$fa-var-bell-o: "\f0a2"; +$fa-var-bitbucket: "\f171"; +$fa-var-bitbucket-square: "\f172"; +$fa-var-bitcoin: "\f15a"; +$fa-var-bold: "\f032"; +$fa-var-bolt: "\f0e7"; +$fa-var-bomb: "\f1e2"; +$fa-var-book: "\f02d"; +$fa-var-bookmark: "\f02e"; +$fa-var-bookmark-o: "\f097"; +$fa-var-briefcase: "\f0b1"; +$fa-var-btc: "\f15a"; +$fa-var-bug: "\f188"; +$fa-var-building: "\f1ad"; +$fa-var-building-o: "\f0f7"; +$fa-var-bullhorn: "\f0a1"; +$fa-var-bullseye: "\f140"; +$fa-var-cab: "\f1ba"; +$fa-var-calendar: "\f073"; +$fa-var-calendar-o: "\f133"; +$fa-var-camera: "\f030"; +$fa-var-camera-retro: "\f083"; +$fa-var-car: "\f1b9"; +$fa-var-caret-down: "\f0d7"; +$fa-var-caret-left: "\f0d9"; +$fa-var-caret-right: "\f0da"; +$fa-var-caret-square-o-down: "\f150"; +$fa-var-caret-square-o-left: "\f191"; +$fa-var-caret-square-o-right: "\f152"; +$fa-var-caret-square-o-up: "\f151"; +$fa-var-caret-up: "\f0d8"; +$fa-var-certificate: "\f0a3"; +$fa-var-chain: "\f0c1"; +$fa-var-chain-broken: "\f127"; +$fa-var-check: "\f00c"; +$fa-var-check-circle: "\f058"; +$fa-var-check-circle-o: "\f05d"; +$fa-var-check-square: "\f14a"; +$fa-var-check-square-o: "\f046"; +$fa-var-chevron-circle-down: "\f13a"; +$fa-var-chevron-circle-left: "\f137"; +$fa-var-chevron-circle-right: "\f138"; +$fa-var-chevron-circle-up: "\f139"; +$fa-var-chevron-down: "\f078"; +$fa-var-chevron-left: "\f053"; +$fa-var-chevron-right: "\f054"; +$fa-var-chevron-up: "\f077"; +$fa-var-child: "\f1ae"; +$fa-var-circle: "\f111"; +$fa-var-circle-o: "\f10c"; +$fa-var-circle-o-notch: "\f1ce"; +$fa-var-circle-thin: "\f1db"; +$fa-var-clipboard: "\f0ea"; +$fa-var-clock-o: "\f017"; +$fa-var-cloud: "\f0c2"; +$fa-var-cloud-download: "\f0ed"; +$fa-var-cloud-upload: "\f0ee"; +$fa-var-cny: "\f157"; +$fa-var-code: "\f121"; +$fa-var-code-fork: "\f126"; +$fa-var-codepen: "\f1cb"; +$fa-var-coffee: "\f0f4"; +$fa-var-cog: "\f013"; +$fa-var-cogs: "\f085"; +$fa-var-columns: "\f0db"; +$fa-var-comment: "\f075"; +$fa-var-comment-o: "\f0e5"; +$fa-var-comments: "\f086"; +$fa-var-comments-o: "\f0e6"; +$fa-var-compass: "\f14e"; +$fa-var-compress: "\f066"; +$fa-var-copy: "\f0c5"; +$fa-var-credit-card: "\f09d"; +$fa-var-crop: "\f125"; +$fa-var-crosshairs: "\f05b"; +$fa-var-css3: "\f13c"; +$fa-var-cube: "\f1b2"; +$fa-var-cubes: "\f1b3"; +$fa-var-cut: "\f0c4"; +$fa-var-cutlery: "\f0f5"; +$fa-var-dashboard: "\f0e4"; +$fa-var-database: "\f1c0"; +$fa-var-dedent: "\f03b"; +$fa-var-delicious: "\f1a5"; +$fa-var-desktop: "\f108"; +$fa-var-deviantart: "\f1bd"; +$fa-var-digg: "\f1a6"; +$fa-var-dollar: "\f155"; +$fa-var-dot-circle-o: "\f192"; +$fa-var-download: "\f019"; +$fa-var-dribbble: "\f17d"; +$fa-var-dropbox: "\f16b"; +$fa-var-drupal: "\f1a9"; +$fa-var-edit: "\f044"; +$fa-var-eject: "\f052"; +$fa-var-ellipsis-h: "\f141"; +$fa-var-ellipsis-v: "\f142"; +$fa-var-empire: "\f1d1"; +$fa-var-envelope: "\f0e0"; +$fa-var-envelope-o: "\f003"; +$fa-var-envelope-square: "\f199"; +$fa-var-eraser: "\f12d"; +$fa-var-eur: "\f153"; +$fa-var-euro: "\f153"; +$fa-var-exchange: "\f0ec"; +$fa-var-exclamation: "\f12a"; +$fa-var-exclamation-circle: "\f06a"; +$fa-var-exclamation-triangle: "\f071"; +$fa-var-expand: "\f065"; +$fa-var-external-link: "\f08e"; +$fa-var-external-link-square: "\f14c"; +$fa-var-eye: "\f06e"; +$fa-var-eye-slash: "\f070"; +$fa-var-facebook: "\f09a"; +$fa-var-facebook-square: "\f082"; +$fa-var-fast-backward: "\f049"; +$fa-var-fast-forward: "\f050"; +$fa-var-fax: "\f1ac"; +$fa-var-female: "\f182"; +$fa-var-fighter-jet: "\f0fb"; +$fa-var-file: "\f15b"; +$fa-var-file-archive-o: "\f1c6"; +$fa-var-file-audio-o: "\f1c7"; +$fa-var-file-code-o: "\f1c9"; +$fa-var-file-excel-o: "\f1c3"; +$fa-var-file-image-o: "\f1c5"; +$fa-var-file-movie-o: "\f1c8"; +$fa-var-file-o: "\f016"; +$fa-var-file-pdf-o: "\f1c1"; +$fa-var-file-photo-o: "\f1c5"; +$fa-var-file-picture-o: "\f1c5"; +$fa-var-file-powerpoint-o: "\f1c4"; +$fa-var-file-sound-o: "\f1c7"; +$fa-var-file-text: "\f15c"; +$fa-var-file-text-o: "\f0f6"; +$fa-var-file-video-o: "\f1c8"; +$fa-var-file-word-o: "\f1c2"; +$fa-var-file-zip-o: "\f1c6"; +$fa-var-files-o: "\f0c5"; +$fa-var-film: "\f008"; +$fa-var-filter: "\f0b0"; +$fa-var-fire: "\f06d"; +$fa-var-fire-extinguisher: "\f134"; +$fa-var-flag: "\f024"; +$fa-var-flag-checkered: "\f11e"; +$fa-var-flag-o: "\f11d"; +$fa-var-flash: "\f0e7"; +$fa-var-flask: "\f0c3"; +$fa-var-flickr: "\f16e"; +$fa-var-floppy-o: "\f0c7"; +$fa-var-folder: "\f07b"; +$fa-var-folder-o: "\f114"; +$fa-var-folder-open: "\f07c"; +$fa-var-folder-open-o: "\f115"; +$fa-var-font: "\f031"; +$fa-var-forward: "\f04e"; +$fa-var-foursquare: "\f180"; +$fa-var-frown-o: "\f119"; +$fa-var-gamepad: "\f11b"; +$fa-var-gavel: "\f0e3"; +$fa-var-gbp: "\f154"; +$fa-var-ge: "\f1d1"; +$fa-var-gear: "\f013"; +$fa-var-gears: "\f085"; +$fa-var-gift: "\f06b"; +$fa-var-git: "\f1d3"; +$fa-var-git-square: "\f1d2"; +$fa-var-github: "\f09b"; +$fa-var-github-alt: "\f113"; +$fa-var-github-square: "\f092"; +$fa-var-gittip: "\f184"; +$fa-var-glass: "\f000"; +$fa-var-globe: "\f0ac"; +$fa-var-google: "\f1a0"; +$fa-var-google-plus: "\f0d5"; +$fa-var-google-plus-square: "\f0d4"; +$fa-var-graduation-cap: "\f19d"; +$fa-var-group: "\f0c0"; +$fa-var-h-square: "\f0fd"; +$fa-var-hacker-news: "\f1d4"; +$fa-var-hand-o-down: "\f0a7"; +$fa-var-hand-o-left: "\f0a5"; +$fa-var-hand-o-right: "\f0a4"; +$fa-var-hand-o-up: "\f0a6"; +$fa-var-hdd-o: "\f0a0"; +$fa-var-header: "\f1dc"; +$fa-var-headphones: "\f025"; +$fa-var-heart: "\f004"; +$fa-var-heart-o: "\f08a"; +$fa-var-history: "\f1da"; +$fa-var-home: "\f015"; +$fa-var-hospital-o: "\f0f8"; +$fa-var-html5: "\f13b"; +$fa-var-image: "\f03e"; +$fa-var-inbox: "\f01c"; +$fa-var-indent: "\f03c"; +$fa-var-info: "\f129"; +$fa-var-info-circle: "\f05a"; +$fa-var-inr: "\f156"; +$fa-var-instagram: "\f16d"; +$fa-var-institution: "\f19c"; +$fa-var-italic: "\f033"; +$fa-var-joomla: "\f1aa"; +$fa-var-jpy: "\f157"; +$fa-var-jsfiddle: "\f1cc"; +$fa-var-key: "\f084"; +$fa-var-keyboard-o: "\f11c"; +$fa-var-krw: "\f159"; +$fa-var-language: "\f1ab"; +$fa-var-laptop: "\f109"; +$fa-var-leaf: "\f06c"; +$fa-var-legal: "\f0e3"; +$fa-var-lemon-o: "\f094"; +$fa-var-level-down: "\f149"; +$fa-var-level-up: "\f148"; +$fa-var-life-bouy: "\f1cd"; +$fa-var-life-ring: "\f1cd"; +$fa-var-life-saver: "\f1cd"; +$fa-var-lightbulb-o: "\f0eb"; +$fa-var-link: "\f0c1"; +$fa-var-linkedin: "\f0e1"; +$fa-var-linkedin-square: "\f08c"; +$fa-var-linux: "\f17c"; +$fa-var-list: "\f03a"; +$fa-var-list-alt: "\f022"; +$fa-var-list-ol: "\f0cb"; +$fa-var-list-ul: "\f0ca"; +$fa-var-location-arrow: "\f124"; +$fa-var-lock: "\f023"; +$fa-var-long-arrow-down: "\f175"; +$fa-var-long-arrow-left: "\f177"; +$fa-var-long-arrow-right: "\f178"; +$fa-var-long-arrow-up: "\f176"; +$fa-var-magic: "\f0d0"; +$fa-var-magnet: "\f076"; +$fa-var-mail-forward: "\f064"; +$fa-var-mail-reply: "\f112"; +$fa-var-mail-reply-all: "\f122"; +$fa-var-male: "\f183"; +$fa-var-map-marker: "\f041"; +$fa-var-maxcdn: "\f136"; +$fa-var-medkit: "\f0fa"; +$fa-var-meh-o: "\f11a"; +$fa-var-microphone: "\f130"; +$fa-var-microphone-slash: "\f131"; +$fa-var-minus: "\f068"; +$fa-var-minus-circle: "\f056"; +$fa-var-minus-square: "\f146"; +$fa-var-minus-square-o: "\f147"; +$fa-var-mobile: "\f10b"; +$fa-var-mobile-phone: "\f10b"; +$fa-var-money: "\f0d6"; +$fa-var-moon-o: "\f186"; +$fa-var-mortar-board: "\f19d"; +$fa-var-music: "\f001"; +$fa-var-navicon: "\f0c9"; +$fa-var-openid: "\f19b"; +$fa-var-outdent: "\f03b"; +$fa-var-pagelines: "\f18c"; +$fa-var-paper-plane: "\f1d8"; +$fa-var-paper-plane-o: "\f1d9"; +$fa-var-paperclip: "\f0c6"; +$fa-var-paragraph: "\f1dd"; +$fa-var-paste: "\f0ea"; +$fa-var-pause: "\f04c"; +$fa-var-paw: "\f1b0"; +$fa-var-pencil: "\f040"; +$fa-var-pencil-square: "\f14b"; +$fa-var-pencil-square-o: "\f044"; +$fa-var-phone: "\f095"; +$fa-var-phone-square: "\f098"; +$fa-var-photo: "\f03e"; +$fa-var-picture-o: "\f03e"; +$fa-var-pied-piper: "\f1a7"; +$fa-var-pied-piper-alt: "\f1a8"; +$fa-var-pied-piper-square: "\f1a7"; +$fa-var-pinterest: "\f0d2"; +$fa-var-pinterest-square: "\f0d3"; +$fa-var-plane: "\f072"; +$fa-var-play: "\f04b"; +$fa-var-play-circle: "\f144"; +$fa-var-play-circle-o: "\f01d"; +$fa-var-plus: "\f067"; +$fa-var-plus-circle: "\f055"; +$fa-var-plus-square: "\f0fe"; +$fa-var-plus-square-o: "\f196"; +$fa-var-power-off: "\f011"; +$fa-var-print: "\f02f"; +$fa-var-puzzle-piece: "\f12e"; +$fa-var-qq: "\f1d6"; +$fa-var-qrcode: "\f029"; +$fa-var-question: "\f128"; +$fa-var-question-circle: "\f059"; +$fa-var-quote-left: "\f10d"; +$fa-var-quote-right: "\f10e"; +$fa-var-ra: "\f1d0"; +$fa-var-random: "\f074"; +$fa-var-rebel: "\f1d0"; +$fa-var-recycle: "\f1b8"; +$fa-var-reddit: "\f1a1"; +$fa-var-reddit-square: "\f1a2"; +$fa-var-refresh: "\f021"; +$fa-var-renren: "\f18b"; +$fa-var-reorder: "\f0c9"; +$fa-var-repeat: "\f01e"; +$fa-var-reply: "\f112"; +$fa-var-reply-all: "\f122"; +$fa-var-retweet: "\f079"; +$fa-var-rmb: "\f157"; +$fa-var-road: "\f018"; +$fa-var-rocket: "\f135"; +$fa-var-rotate-left: "\f0e2"; +$fa-var-rotate-right: "\f01e"; +$fa-var-rouble: "\f158"; +$fa-var-rss: "\f09e"; +$fa-var-rss-square: "\f143"; +$fa-var-rub: "\f158"; +$fa-var-ruble: "\f158"; +$fa-var-rupee: "\f156"; +$fa-var-save: "\f0c7"; +$fa-var-scissors: "\f0c4"; +$fa-var-search: "\f002"; +$fa-var-search-minus: "\f010"; +$fa-var-search-plus: "\f00e"; +$fa-var-send: "\f1d8"; +$fa-var-send-o: "\f1d9"; +$fa-var-share: "\f064"; +$fa-var-share-alt: "\f1e0"; +$fa-var-share-alt-square: "\f1e1"; +$fa-var-share-square: "\f14d"; +$fa-var-share-square-o: "\f045"; +$fa-var-shield: "\f132"; +$fa-var-shopping-cart: "\f07a"; +$fa-var-sign-in: "\f090"; +$fa-var-sign-out: "\f08b"; +$fa-var-signal: "\f012"; +$fa-var-sitemap: "\f0e8"; +$fa-var-skype: "\f17e"; +$fa-var-slack: "\f198"; +$fa-var-sliders: "\f1de"; +$fa-var-smile-o: "\f118"; +$fa-var-sort: "\f0dc"; +$fa-var-sort-alpha-asc: "\f15d"; +$fa-var-sort-alpha-desc: "\f15e"; +$fa-var-sort-amount-asc: "\f160"; +$fa-var-sort-amount-desc: "\f161"; +$fa-var-sort-asc: "\f0de"; +$fa-var-sort-desc: "\f0dd"; +$fa-var-sort-down: "\f0dd"; +$fa-var-sort-numeric-asc: "\f162"; +$fa-var-sort-numeric-desc: "\f163"; +$fa-var-sort-up: "\f0de"; +$fa-var-soundcloud: "\f1be"; +$fa-var-space-shuttle: "\f197"; +$fa-var-spinner: "\f110"; +$fa-var-spoon: "\f1b1"; +$fa-var-spotify: "\f1bc"; +$fa-var-square: "\f0c8"; +$fa-var-square-o: "\f096"; +$fa-var-stack-exchange: "\f18d"; +$fa-var-stack-overflow: "\f16c"; +$fa-var-star: "\f005"; +$fa-var-star-half: "\f089"; +$fa-var-star-half-empty: "\f123"; +$fa-var-star-half-full: "\f123"; +$fa-var-star-half-o: "\f123"; +$fa-var-star-o: "\f006"; +$fa-var-steam: "\f1b6"; +$fa-var-steam-square: "\f1b7"; +$fa-var-step-backward: "\f048"; +$fa-var-step-forward: "\f051"; +$fa-var-stethoscope: "\f0f1"; +$fa-var-stop: "\f04d"; +$fa-var-strikethrough: "\f0cc"; +$fa-var-stumbleupon: "\f1a4"; +$fa-var-stumbleupon-circle: "\f1a3"; +$fa-var-subscript: "\f12c"; +$fa-var-suitcase: "\f0f2"; +$fa-var-sun-o: "\f185"; +$fa-var-superscript: "\f12b"; +$fa-var-support: "\f1cd"; +$fa-var-table: "\f0ce"; +$fa-var-tablet: "\f10a"; +$fa-var-tachometer: "\f0e4"; +$fa-var-tag: "\f02b"; +$fa-var-tags: "\f02c"; +$fa-var-tasks: "\f0ae"; +$fa-var-taxi: "\f1ba"; +$fa-var-tencent-weibo: "\f1d5"; +$fa-var-terminal: "\f120"; +$fa-var-text-height: "\f034"; +$fa-var-text-width: "\f035"; +$fa-var-th: "\f00a"; +$fa-var-th-large: "\f009"; +$fa-var-th-list: "\f00b"; +$fa-var-thumb-tack: "\f08d"; +$fa-var-thumbs-down: "\f165"; +$fa-var-thumbs-o-down: "\f088"; +$fa-var-thumbs-o-up: "\f087"; +$fa-var-thumbs-up: "\f164"; +$fa-var-ticket: "\f145"; +$fa-var-times: "\f00d"; +$fa-var-times-circle: "\f057"; +$fa-var-times-circle-o: "\f05c"; +$fa-var-tint: "\f043"; +$fa-var-toggle-down: "\f150"; +$fa-var-toggle-left: "\f191"; +$fa-var-toggle-right: "\f152"; +$fa-var-toggle-up: "\f151"; +$fa-var-trash-o: "\f014"; +$fa-var-tree: "\f1bb"; +$fa-var-trello: "\f181"; +$fa-var-trophy: "\f091"; +$fa-var-truck: "\f0d1"; +$fa-var-try: "\f195"; +$fa-var-tumblr: "\f173"; +$fa-var-tumblr-square: "\f174"; +$fa-var-turkish-lira: "\f195"; +$fa-var-twitter: "\f099"; +$fa-var-twitter-square: "\f081"; +$fa-var-umbrella: "\f0e9"; +$fa-var-underline: "\f0cd"; +$fa-var-undo: "\f0e2"; +$fa-var-university: "\f19c"; +$fa-var-unlink: "\f127"; +$fa-var-unlock: "\f09c"; +$fa-var-unlock-alt: "\f13e"; +$fa-var-unsorted: "\f0dc"; +$fa-var-upload: "\f093"; +$fa-var-usd: "\f155"; +$fa-var-user: "\f007"; +$fa-var-user-md: "\f0f0"; +$fa-var-users: "\f0c0"; +$fa-var-video-camera: "\f03d"; +$fa-var-vimeo-square: "\f194"; +$fa-var-vine: "\f1ca"; +$fa-var-vk: "\f189"; +$fa-var-volume-down: "\f027"; +$fa-var-volume-off: "\f026"; +$fa-var-volume-up: "\f028"; +$fa-var-warning: "\f071"; +$fa-var-wechat: "\f1d7"; +$fa-var-weibo: "\f18a"; +$fa-var-weixin: "\f1d7"; +$fa-var-wheelchair: "\f193"; +$fa-var-windows: "\f17a"; +$fa-var-won: "\f159"; +$fa-var-wordpress: "\f19a"; +$fa-var-wrench: "\f0ad"; +$fa-var-xing: "\f168"; +$fa-var-xing-square: "\f169"; +$fa-var-yahoo: "\f19e"; +$fa-var-yen: "\f157"; +$fa-var-youtube: "\f167"; +$fa-var-youtube-play: "\f16a"; +$fa-var-youtube-square: "\f166"; + diff --git a/saserverlibrary/src/main/resources/font-awesome/scss/font-awesome.scss b/saserverlibrary/src/main/resources/font-awesome/scss/font-awesome.scss new file mode 100644 index 0000000..2307dbd --- /dev/null +++ b/saserverlibrary/src/main/resources/font-awesome/scss/font-awesome.scss @@ -0,0 +1,17 @@ +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@import "variables"; +@import "mixins"; +@import "path"; +@import "core"; +@import "larger"; +@import "fixed-width"; +@import "list"; +@import "bordered-pulled"; +@import "spinning"; +@import "rotated-flipped"; +@import "stacked"; +@import "icons"; diff --git a/saserverlibrary/src/main/resources/iamRoutes.conf b/saserverlibrary/src/main/resources/iamRoutes.conf new file mode 100644 index 0000000..2fb7329 --- /dev/null +++ b/saserverlibrary/src/main/resources/iamRoutes.conf @@ -0,0 +1,8 @@ +# +# API Keys +# +GET /v1/apiKeys com.att.nsa.apiServer.endpoints.NsaApiKeyEndpoint.getAllApiKeys +POST /v1/apiKeys/create com.att.nsa.apiServer.endpoints.NsaApiKeyEndpoint.createApiKey +GET /v1/apiKeys/{apiKey} com.att.nsa.apiServer.endpoints.NsaApiKeyEndpoint.getApiKey +PATCH /v1/apiKeys/{apiKey} com.att.nsa.apiServer.endpoints.NsaApiKeyEndpoint.updateApiKey +DELETE /v1/apiKeys/{apiKey} com.att.nsa.apiServer.endpoints.NsaApiKeyEndpoint.deleteApiKey diff --git a/saserverlibrary/src/main/resources/images/attLogo.gif b/saserverlibrary/src/main/resources/images/attLogo.gif new file mode 100644 index 0000000..10f184c Binary files /dev/null and b/saserverlibrary/src/main/resources/images/attLogo.gif differ diff --git a/saserverlibrary/src/main/resources/images/att_vt_1cp_grd_rev.gif b/saserverlibrary/src/main/resources/images/att_vt_1cp_grd_rev.gif new file mode 100644 index 0000000..034515c Binary files /dev/null and b/saserverlibrary/src/main/resources/images/att_vt_1cp_grd_rev.gif differ diff --git a/saserverlibrary/src/main/resources/images/dimmer.png b/saserverlibrary/src/main/resources/images/dimmer.png new file mode 100644 index 0000000..a32767b Binary files /dev/null and b/saserverlibrary/src/main/resources/images/dimmer.png differ diff --git a/saserverlibrary/src/main/resources/images/dimmer.xcf b/saserverlibrary/src/main/resources/images/dimmer.xcf new file mode 100644 index 0000000..8d1a275 Binary files /dev/null and b/saserverlibrary/src/main/resources/images/dimmer.xcf differ diff --git a/saserverlibrary/src/main/resources/templates/error.html b/saserverlibrary/src/main/resources/templates/error.html new file mode 100644 index 0000000..fbafab9 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/error.html @@ -0,0 +1,13 @@ + +#parse ( "header.html" ) + +

+ #if ( $msg ) + $msg + #else + An error occurred; no further information is available. + #end +
+ + +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/footer.html b/saserverlibrary/src/main/resources/templates/footer.html new file mode 100644 index 0000000..de9441c --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/footer.html @@ -0,0 +1,12 @@ + + + + + + + #foreach ( $script in $footerScripts ) + + #end + + + diff --git a/saserverlibrary/src/main/resources/templates/header.html b/saserverlibrary/src/main/resources/templates/header.html new file mode 100644 index 0000000..61eaf79 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/header.html @@ -0,0 +1,70 @@ + + + + AT&T 2020 Systems Control + + + + + + + + + + + + #foreach ( $script in $headerScripts ) + + #end + + + #foreach ( $plugin in $plugins ) + + #foreach ( $cssName in $plugin.getUiCssList() ) + + #end + #end + + + + + + + #if($preContainerParse) + #parse($preContainerParse) + #end + +
+ + #foreach ( $msg in $!messages ) + + #end + diff --git a/saserverlibrary/src/main/resources/templates/main.html b/saserverlibrary/src/main/resources/templates/main.html new file mode 100644 index 0000000..cc717fd --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/main.html @@ -0,0 +1,6 @@ + +#parse ( "header.html" ) + +

Choose a menu item above.

+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/metrics.html b/saserverlibrary/src/main/resources/templates/metrics.html new file mode 100644 index 0000000..8537418 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/metrics.html @@ -0,0 +1,29 @@ + +#set( $tab = "metrics" ) +#parse ( "header.html" ) + + + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
+ #foreach ( $entry in $metricEntries ) +
+ + #if ( $entry.hasValue() ) + $entry.localName: $entry.value.summarize() + #end + +
+ #end +
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/_cgListing.html b/saserverlibrary/src/main/resources/templates/older/_cgListing.html new file mode 100644 index 0000000..0120174 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/_cgListing.html @@ -0,0 +1,43 @@ + + #if ( !$cg.alarmTraversalConsistent || !$cg.parentTraversalConsistent ) + #set ( $inconsistent = true ) + #else + #set ( $inconsistent = false ) + #end + +
#if ( $inconsistent ) #end $cg.name
+
+
+
Anchor:
+ #if ( !$cg.alarmTraversalConsistent ) +
The traversals for this alarm set are not consistent!
+ #else +
$cg.anchorTopologyType.simpleName
+ #end +
+ +
+
Trigger:
+
$cg.trigger.describe()
+
+ +
Alarms:
+ #foreach ( $ar in $cg.alarmRecords ) +
+
$ar.label
+
$ar.alarmSpecification
+
@ $ar.traverser.describeReverse()
+
+ #end + + #if ( $cg.parentRecords.size() > 0 ) + #set ( $bps = $cg.badParents ) +
Secondary To:
+ #foreach ( $parent in $cg.parentRecords ) +
+ +
@ $parent.traverser.describeForward()
+
+ #end + #end +
diff --git a/saserverlibrary/src/main/resources/templates/older/activeAlarm.html b/saserverlibrary/src/main/resources/templates/older/activeAlarm.html new file mode 100644 index 0000000..addb186 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/activeAlarm.html @@ -0,0 +1,31 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + + #if ( $alarm ) + +
Alarm: $alarm.identifier
+ + #if ( $correlation ) +
Correlated in $correlation.id
+ #end + + + + #foreach ( $field in $fields ) + + #end +
$field$alarm.getValueViaSchema($field,"")
+ #else +
Alarm
+
The alarm was not found. (Did it clear?).
+ #end + +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/activeCg.html b/saserverlibrary/src/main/resources/templates/older/activeCg.html new file mode 100644 index 0000000..27b525c --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/activeCg.html @@ -0,0 +1,27 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Active Correlation
+ + #if ( $activeCg ) + + + + +
Secondaries:
+ #foreach ( $secondary in $activeCg.secondaries ) + + #end + #else +
The correlation was not found. It may have cleared or been subsumed by another correlation.
+ #end + +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/cg.html b/saserverlibrary/src/main/resources/templates/older/cg.html new file mode 100644 index 0000000..848c1b9 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/cg.html @@ -0,0 +1,16 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Correlation Group Detail
+ + #parse("_cgListing.html") + +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/cons.html b/saserverlibrary/src/main/resources/templates/older/cons.html new file mode 100644 index 0000000..d0ecaa2 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/cons.html @@ -0,0 +1,65 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Consolidator
+ + +
+ #if ( $alarm ) +
$alarm.identifier
+ #if ( $cons.alarmDb.getImpactsFor($alarm.identifier).size()>0 ) +
+
Alarms to re-consolidate if this alarm changes:
+ #foreach ( $impact in $cons.alarmDb.getImpactsFor($alarm.identifier) ) + + #end +
+ #end +
+ + + + + + + + #foreach ( $field in $alarm.allFields ) + #set ( $doneOne = false ) + #set ( $oddRow = true ) + #foreach ( $entry in $alarm.getAllOpinionsOn($field).entrySet() ) + + + + + + + #set ( $doneOne = true ) + #if ( $oddRow ) #set($oddRow=false) #else #set($oddRow=true) #end + #end + #end +
fieldsourcevalueconsolidated
#if(!$doneOne)$field#else #end$entry.getKey()$!entry.getValue()#if(!$doneOne)$!consAlarm.getValue($field)#end
+
+ #else +
Alarm DB
+
+
+ #foreach ( $c in $cons.alarmDb.allAlarms ) +
+ #if ( $c.hasConflicts() ) #end + $c.identifier +
+ #end +
+
+ #end +
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/corr.html b/saserverlibrary/src/main/resources/templates/older/corr.html new file mode 100644 index 0000000..5c5601d --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/corr.html @@ -0,0 +1,54 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Configurable Alarm Correlator
+ +
+
+ +
+ +
Active Correlations
+
+
+ #foreach ( $c in $cce.alarmState.allCorrelations ) + + #end +
+
+ +
Active Alarms
+
+
Active Alarms: $!cce.alarmState.size()
+ #foreach ( $a in $cce.alarmState.allAlarms ) +
+ $a + +
+ #end +
+ +
Correlator Metrics
+
+
Total Events: $!cce.metrics.totalEvents ($!cce.metrics.alarmRate)
+
Onsets: $!cce.metrics.totalOnsets
+
Clears: $!cce.metrics.totalClears
+
Correlations: $!cce.metrics.totalCorrelations
+
+
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/corrEvent.html b/saserverlibrary/src/main/resources/templates/older/corrEvent.html new file mode 100644 index 0000000..b714fbd --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/corrEvent.html @@ -0,0 +1,34 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Event $event.identifier
+ +
+
$event.eventId
+
$event.topoKey
+ + #if ( $activeCg ) + #if ( $activeCg.primary == $event ) + +
+
Secondaries
+ #foreach ( $secondary in $activeCg.secondaries ) + + #end +
+ #else + + + #end + #end +
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/injector.html b/saserverlibrary/src/main/resources/templates/older/injector.html new file mode 100644 index 0000000..15b94a3 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/injector.html @@ -0,0 +1,26 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Alarm Injection
+
+ You can inject alarms into the correlation system. Alarms will be marked as a test + alarms, so they will not be published by the UCA value pack. +
+
+
+
$!alarmDataInvalid
+
+
+
+
+
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/topo.html b/saserverlibrary/src/main/resources/templates/older/topo.html new file mode 100644 index 0000000..9dceb88 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/topo.html @@ -0,0 +1,20 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Alarms on $topoKey
+ +
+ #foreach ( $alarm in $alarms ) + + #end +
+ +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/templates/older/traverse.html b/saserverlibrary/src/main/resources/templates/older/traverse.html new file mode 100644 index 0000000..d502267 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/traverse.html @@ -0,0 +1,60 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + +
Tests
+ + +
+
+
+
+ +
+
+
+ +
 
+
+
Results
+ #foreach ( $r in $targets ) +
$r
+ #end +
+ +
+ +#parse ( "footer.html" ) + + diff --git a/saserverlibrary/src/main/resources/templates/older/wm.html b/saserverlibrary/src/main/resources/templates/older/wm.html new file mode 100644 index 0000000..3222bb7 --- /dev/null +++ b/saserverlibrary/src/main/resources/templates/older/wm.html @@ -0,0 +1,36 @@ + +#parse ( "header.html" ) + +
+ + #if ( $errorMsg ) +
$errorMsg
+ #end + + #if ( $alarm ) + #* display a single alarm *# +
Alarm Inspection: $alarm.identifier
+ + #foreach ( $field in $fields ) + + #end +
$field$alarm.getValueViaSchema($field,"")
+ #elseif ( $alarms ) + #* display all alarms *# +
Alarms in UCA's Working Memory
+
Note that LTE does not keep alarms in UCA's Working Memory.
+ + #foreach ( $alarm in $alarms ) + + #end +
$alarm.identifier$alarm.getValueViaSchema("AlarmID","???") on $alarm.getValueViaSchema("ManagedObjectInstance","(no topo)")
+ #else +
Alarms Inspection
+
Alarm(s) not found
+ #end + + + +
+ +#parse ( "footer.html" ) diff --git a/saserverlibrary/src/main/resources/uiRoutes.conf b/saserverlibrary/src/main/resources/uiRoutes.conf new file mode 100644 index 0000000..3c28001 --- /dev/null +++ b/saserverlibrary/src/main/resources/uiRoutes.conf @@ -0,0 +1,40 @@ + +package com.att.nsa.ui.endpoints + +# a simple entry point +GET / UiMain.getMain +GET /index redirect:/ +GET /index.htm redirect:/ +GET /index.html redirect:/ + +# typical static file paths +GET /css/ staticDir:css +GET /js/ staticDir:js +GET /images/ staticDir:images +GET /font/ staticDir:font +GET /favicon.ico staticFile:images/attLogo.gif +GET /font-awesome/ staticDir:font-awesome + +# icon handling +GET /apple-touch-icon-114x114.png staticFile:icons/ +GET /apple-touch-icon-120x120.png staticFile:icons/ +GET /apple-touch-icon-144x144.png staticFile:icons/ +GET /apple-touch-icon-152x152.png staticFile:icons/ +GET /apple-touch-icon-57x57.png staticFile:icons/ +GET /apple-touch-icon-60x60.png staticFile:icons/ +GET /apple-touch-icon-72x72.png staticFile:icons/ +GET /apple-touch-icon-76x76.png staticFile:icons/ +GET /apple-touch-icon-precomposed.png staticFile:icons/ +GET /apple-touch-icon.png staticFile:icons/ +GET /browserconfig.xml staticFile:icons/ +GET /favicon-160x160.png staticFile:icons/ +GET /favicon-16x16.png staticFile:icons/ +GET /favicon-196x196.png staticFile:icons/ +GET /favicon-32x32.png staticFile:icons/ +GET /favicon-96x96.png staticFile:icons/ +GET /favicon.ico staticFile:icons/ +GET /mstile-144x144.png staticFile:icons/ +GET /mstile-150x150.png staticFile:icons/ +GET /mstile-310x150.png staticFile:icons/ +GET /mstile-310x310.png staticFile:icons/ +GET /mstile-70x70.png staticFile:icons/ diff --git a/saserverlibrary/src/main/sass/.gitignore b/saserverlibrary/src/main/sass/.gitignore new file mode 100644 index 0000000..5df1b9b --- /dev/null +++ b/saserverlibrary/src/main/sass/.gitignore @@ -0,0 +1 @@ +.sass-cache diff --git a/saserverlibrary/src/main/sass/config.rb b/saserverlibrary/src/main/sass/config.rb new file mode 100644 index 0000000..a3b2040 --- /dev/null +++ b/saserverlibrary/src/main/sass/config.rb @@ -0,0 +1,24 @@ +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "../resources/css" +sass_dir = "sass" +images_dir = "images" +javascripts_dir = "javascripts" + +# You can select your preferred output style here (can be overridden via the command line): +# output_style = :expanded or :nested or :compact or :compressed + +# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +# line_comments = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass diff --git a/saserverlibrary/src/main/sass/sass/_commonSettings.scss b/saserverlibrary/src/main/sass/sass/_commonSettings.scss new file mode 100644 index 0000000..47fd2dd --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/_commonSettings.scss @@ -0,0 +1,39 @@ + +// from the official AT&T palette 2014 +$colorAttOrange: #EF6F00; +$colorAttBlue: #067AB4; +$colorAttGreenHighlight: #C4D82D; +$colorAttGreen: #4CA90C; +$colorAttBlueHighlight: #44C8F5; +$colorAttBurgundy: #B30A3C; +$colorAttMagenta: #DA0081; +$colorAttOrangeHighlight: #FCB314; +$colorAttDarkBlue: #0C2577; +$colorAttPurple: #81017E; +$colorAttGray: #666666; + +$colorWhite: #fff; +$colorBlack: #000; + +//$totalWidth: 1080px; +$totalWidth: 960px; +$totalHeight: 30em; + +$linkColor: #448; + +$linkHiliteColor: #6FBAF7; + +$topBarColor: $colorWhite; +$topBarTextColor: $colorAttGray; +$topBarActiveTextColor: $colorAttBlue; +$topBarLinkHiliteColor: $colorAttBlueHighlight; + +$bodyColor: #fff; + +$footerColor: #fff; +$footerTextColor: #333; + +$bodyTextColor: #222; +$labelGray: #444; + + diff --git a/saserverlibrary/src/main/sass/sass/_forms.scss b/saserverlibrary/src/main/sass/sass/_forms.scss new file mode 100644 index 0000000..822f933 --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/_forms.scss @@ -0,0 +1,35 @@ + +#injectForm +{ + margin-top: 2em; + width: 100%; + position: relative; +} + +.injectLabel +{ + display: inline-block; + width: 6em; + + text-align: right; + padding-right: 1em; + + vertical-align: top; +} + +#alarmData +{ + wrap: off; + + width: 600px; + height: 120px; + border: 3px solid #cccccc; + + padding: 0.25em; +} + +.injectError +{ + color: #ff0000; +} + diff --git a/saserverlibrary/src/main/sass/sass/_header.scss b/saserverlibrary/src/main/sass/sass/_header.scss new file mode 100644 index 0000000..332c945 --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/_header.scss @@ -0,0 +1,94 @@ + +#topbar-wrap +{ + width: 100%; + background: $topBarColor; +} + +#topbar +{ + width: $totalWidth; + margin: 0 auto; + + padding-top: 0.75em; + padding-bottom: 0.5em; + + position: relative; /* for absolute positioning internally */ + + font-family: Courier, Arial; /* OmnesATT */ + font-size: 1.0em; + color: $topBarTextColor; + + a.selectedTab + { + text-decoration: none; + color: $topBarActiveTextColor; + &:hover + { + color: $topBarLinkHiliteColor; + text-decoration: none; + } + } + + a.unselectedTab + { + text-decoration: none; + color: $topBarTextColor; + &:hover + { + color: $topBarLinkHiliteColor; + text-decoration: none; + } + } + + ul + { + list-style: none; + + li + { + display: inline; + margin: 0 0.5em 0 0; + } + } +} + +#searchBarWrap +{ + border: 2px #aaa solid; + border-radius: 15px; + background-color: lighten(#444,10%); + + padding-left: 0.75em; + padding-right: 0.75em; + + input + { + border: none; + + padding-left: 0.5em; + padding-right: 0.5em; + + color: #fff; + background-color: lighten(#444,10%); + + &:focus + { + outline: none; + } + } +} + +.dialog-info +{ + display: none; + + padding-top: 1em; + padding-bottom: 1em; + + color: #444; + background-color: $linkHiliteColor; + + text-align: center; +} + diff --git a/saserverlibrary/src/main/sass/sass/ie.scss b/saserverlibrary/src/main/sass/sass/ie.scss new file mode 100644 index 0000000..5cd5b6c --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/ie.scss @@ -0,0 +1,5 @@ +/* Welcome to Compass. Use this file to write IE specific override styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/saserverlibrary/src/main/sass/sass/print.scss b/saserverlibrary/src/main/sass/sass/print.scss new file mode 100644 index 0000000..b0e9e45 --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/print.scss @@ -0,0 +1,3 @@ +/* Welcome to Compass. Use this file to define print styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/saserverlibrary/src/main/sass/sass/screen.scss b/saserverlibrary/src/main/sass/sass/screen.scss new file mode 100644 index 0000000..a5a987f --- /dev/null +++ b/saserverlibrary/src/main/sass/sass/screen.scss @@ -0,0 +1,581 @@ +@charset "utf-8"; + +@import "compass/reset"; +@import "commonSettings"; + +@import "header"; + +body +{ + -webkit-font-smoothing:antialiased; + font-family: Helvetica, Arial, sans; + font-size:1em; + line-height:1.5em; + letter-spacing:1px; + text-rendering:optimizeLegibility; + + background-color: $footerColor; + color: $bodyTextColor; +} + +/* ************************************************************************* */ +/* ************************************************************************* */ + +#bottom-wrap +{ + width: 100%; + background: $footerColor; + padding-top: 1em; + padding-bottom: 1em; +} + +#bottom +{ + width: $totalWidth; + margin: 0 auto; + position: relative; + + font-family: 'Open Sans'; + font-size: 0.8em; + line-height: 1.5em; + + color: $footerTextColor; +} + +.footer-block +{ + display: inline-block; + vertical-align:top; +} + +.footer-text-block +{ + margin-left: 4em; +} + +.footer-text +{ + margin: 0; + padding: 0; + + a + { + text-decoration: none; + color: $footerTextColor; + + &:hover + { + text-decoration: underline; + } + } +} + +/* ************************************************************************* */ +/* ************************************************************************* */ + +#body-wrap +{ + width: 100%; + background-color: $bodyColor; +} + +#body +{ + width: $totalWidth; + min-height: 500px; + margin:0 auto; + padding-top: 1em; +} + +.dev-note +{ + font-size: 60%; + border: 1px dashed gray; + background: #eee; + color: #000; + padding-left: 1em; + padding-top: 0.25em; + padding-bottom: 0.25em; +} + +@import "forms"; + +.title +{ + font-size: 1.5em; +} + +.sectionHeader +{ + padding-top: 1em; + + font-size: 1.25em; +} + +.section +{ + padding-top: 1em; + + a + { + color: $linkColor; + text-decoration: none; + + &:hover + { + color: #666; + text-decoration: none; + } + } +} + +.sectionFooter +{ + font-family: Arial; + font-size: 0.7em; +} + +.section-control +{ + margin: 0; + + &:hover + { + text-decoration: underline; + } +} + +.section-controlpad +{ + font-family: Courier; + font-size: 0.8em; + + a + { + color: $linkColor; + text-decoration: none; + &:hover + { + color: #666; + text-decoration: none; + } + } +} + +.control-pad +{ + padding-top: 0.25em; + padding-bottom: 0.25em; + + ul + { + list-style: none; + + li + { + display: inline; + margin: 0 0.25em 0 0; + } + } + + a + { + color: #333; + text-decoration: none; + + &:hover + { + color: #666; + text-decoration: none; + } + } +} + +.hideable +{ + display: none; +} + +$hpMenuWidth: 125px; +$hpPageWidth: $totalWidth - $hpMenuWidth - 30px; +$hpSettingsPadWidth: 225px; +$hpSettingsBlockWidth: $hpPageWidth - $hpSettingsPadWidth - 15px; +$hpSettingsBlockEntryPadWidth: 30px; +$hpSettingsBlockEntryWidth: $hpSettingsBlockWidth - $hpSettingsBlockEntryPadWidth - 40px; + + +#hpSelectorPad +{ + display: inline-block; + + width: $hpMenuWidth - 2px; + padding-right: 2px; + + vertical-align: top; + overflow: auto; +} + +.hpPage +{ + display: inline-block; + width: $hpPageWidth; + vertical-align: top; +} + +.settings-list +{ + margin-top: 1em; + margin-bottom: 1em; + + font-family: Courier, Arial; + font-size: 0.8em; +} + +.settings-group +{ + border-bottom: 1px dashed gray; + + font-family: Courier, Arial; /* OmnesATT */ + font-size: 0.7em; +} + +.settings-key +{ + display: inline-block; + width: $hpSettingsPadWidth; + overflow: auto; + vertical-align: top; + + font-weight: bold; +} + +.settings-block +{ + display: inline-block; + width: $hpSettingsBlockWidth; + overflow: auto; + vertical-align: top; +} + +.settings-key-group +{ + display: inline-block; + vertical-align: top; + width: $hpSettingsBlockEntryWidth; +} + +.settings-key-group-control +{ + display: inline-block; + vertical-align: top; + width: $hpSettingsBlockEntryPadWidth; +} + +.settings-text +{ +} + +.settings-filter +{ +} + +.settings-value +{ +} + +.section-intro +{ + font-family: Arial; /* OmnesATT */ + font-size: 0.8em; + line-height: 1.25em; + padding-bottom: 0.5em; +} + +.phaseLinkMenu +{ + margin-top: 1.0em; + + font-family: Arial; + font-size: 0.8em; +} + +.processing-list +{ + margin-top: 1em; + margin-bottom: 1em; + + font-family: Courier, Arial; +} + +.processing-entry +{ + display: inline-block; + + overflow: auto; + vertical-align: top; + + padding-top: 1.5em; + padding-bottom: 0.5em; + + font-family: Courier, Arial; + font-size: 0.75em; +} + +.processing-filterpart +{ +} + +.processing-processingpart +{ +} + +.processing-processor +{ + margin-left: 3.0em; +} + +.processing-processor-even +{ + background-color: #fff; +} + +.processing-processor-odd +{ + background-color: #eee; +} + +.processing-entry-controlPadContainer +{ + height: 2em; +} + +.table +{ + margin-top: 1.5em; + font-size: 0.8em; +} + +.tableRecord +{ +} + +.tableLabel +{ + color: #888; + padding-left: 0.25em; + padding-right: 0.25em; + + a + { + text-decoration: none; + color: $linkColor; + + &:hover + { + text-decoration: underline; + } + } +} + +.tableValue +{ + color: #000; + padding-left: 0.25em; + padding-right: 0.25em; +} + +.corr-list +{ + margin-top: 1em; + margin-bottom: 1em; + + font-family: Arial; /* OmnesATT */ +} + +.cg-label +{ + color: $labelGray; +} + +.cg-inlineLabel +{ + display: inline-block; +} + +.cg-section +{ + padding-top: 2.0em; + + font-family: Arial; /* OmnesATT */ + font-size: 1.75em; + font-weight: bold; + + a + { + text-decoration: none; + color: $bodyTextColor; + + &:hover + { + text-decoration: underline; + color: $bodyTextColor; + } + } +} + +.cg-detail +{ + font-size: 0.9em; + + a + { + text-decoration: none; + color: $linkColor; + + &:hover + { + text-decoration: underline; + } + } +} + +.cg-occursOn +{ +} + +.cg-occursOnDetail +{ + display: inline-block; +} + +.cg-trigger +{ +} + +.cg-triggerdetail +{ + display: inline-block; +} + +.cg-alarmRecord +{ +} + +.cg-alarmRecordLabel +{ + display: inline-block; + padding-left: 1.5em; +} + +.cg-alarmRecordSpec +{ + display: inline-block; +} + +.cg-alarmRecordTraversal +{ + display: inline-block; +} + +.cg-alarmRecordCritical +{ + color: #ff0000; +} + +.cg-alarmRecordWarning +{ + color: #fed85d; +} + +.cg-parentRecordName +{ + display: inline-block; + padding-left: 1.5em; +} + +.cg-parentRecordTraversal +{ + display: inline-block; +} + +.activeAlarm +{ +} + +.consAlarmField +{ +} + +.consAlarmValue +{ + padding-left: 2em; +} + +.consConflictedValue +{ + color: #f00; +} + +.consValueKey +{ + display: inline-block; + min-width: 5em; +} + +.consValueValue +{ + display: inline-block; + min-width: 10em; +} + +.consOddRow +{ +} + +.consEvenRow +{ + background-color: #cec; +} + +.consHeaderCell +{ + padding-left: 0.25em; + padding-right: 1em; + font-weight: bold; +} + +.consValueCell +{ + padding-left: 0.25em; + padding-right: 1em; + font-size: 0.9em; +} + +.smaller +{ + font-size: 0.8em; +} + + +#editor +{ + visibility: hidden; + position: absolute; + left: 0px; + top: 0px; + width:100%; + height:100%; + text-align:center; + z-index: 1000; + background-image:url(../images/dimmer.png); +} + +#editor .editor-pane +{ + width:300px; + margin: 100px auto; + background-color: #fff; + border:1px solid #000; + padding:15px; + text-align:center; +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/apiServer/ApiServerTest.java b/saserverlibrary/src/test/java/com/att/nsa/apiServer/ApiServerTest.java new file mode 100644 index 0000000..c34fa2b --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/apiServer/ApiServerTest.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.apiServer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; + +import javax.servlet.Servlet; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import junit.framework.TestCase; + +import org.apache.catalina.LifecycleException; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.att.nsa.util.StreamTools; + +public class ApiServerTest extends TestCase +{ + @Test + public void testHtpsOnly () throws IOException, LifecycleException + { + final File tmpFile = File.createTempFile ( "keyfile", "" ); + try + ( + final InputStream is = ApiServerTest.class.getClassLoader().getResourceAsStream ( "keystore.dummy" ); + final FileOutputStream os = new FileOutputStream ( tmpFile ); + ) + { + StreamTools.copyStream ( is, os ); + } + + final ApiServerConnector conn = new ApiServerConnector.Builder ( 7443 ) + .secure ( true ) + .keystoreFile ( tmpFile.getAbsolutePath () ) + .keystorePassword ( "foobar" ) + .keyAlias ( "tomcat" ) + .build () + ; + + final ApiServer server = new ApiServer.Builder ( new MyServlet () ) + .withConnector ( conn ) + .build () + ; + + server.start (); + + server.stop (); + tmpFile.delete (); + } + + private class MyServlet implements Servlet + { + @Override + public void init ( ServletConfig config ) throws ServletException {} + + @Override + public ServletConfig getServletConfig () + { + return new ServletConfig () + { + @Override + public String getServletName () + { + return "test"; + } + + @Override + public ServletContext getServletContext () + { + return null; + } + + @Override + public String getInitParameter ( String name ) + { + return null; + } + + @Override + public Enumeration getInitParameterNames () + { + return new Enumeration () + { + @Override + public boolean hasMoreElements () { return false; } + + @Override + public String nextElement () { return null; } + }; + } + + }; + } + + @Override + public void service ( ServletRequest req, ServletResponse res ) throws ServletException, IOException + { + log.info ( "service ... " ); + } + + @Override + public String getServletInfo () { return null; } + + @Override + public void destroy () {} + } + + private static final Logger log = LoggerFactory.getLogger ( ApiServerTest.class ); +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/MemConfigDbTest.java b/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/MemConfigDbTest.java new file mode 100644 index 0000000..070f777 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/MemConfigDbTest.java @@ -0,0 +1,54 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.configs.confimpl; + +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.configs.ConfigPath; + +public class MemConfigDbTest extends TestCase +{ + @Test + public void testDbStorage () + { + final String data = "this is some data"; + final MemConfigDb db = new MemConfigDb (); + db.store ( SimplePath.parse("/a/b/c"), data ); + final String readdata = db.load ( SimplePath.parse("/a/b/c/") ); + assertEquals ( data, readdata ); + + final SimplePath path = SimplePath.parse ( "/a/b/c" ); + db.clear ( path ); + assertFalse ( db.exists ( path )); + } + + @Test + public void testChildLookup () + { + final MemConfigDb db = new MemConfigDb (); + for ( int i=0; i<10; i++ ) + { + db.store ( SimplePath.parse("/a/b/c/" + i), "data-"+i ); + db.store ( SimplePath.parse("/a/d/e/" + i), "data-"+(i*10) ); + } + + final Set children = db.loadChildrenNames ( SimplePath.parse("/a/b/c") ); + assertEquals ( 10, children.size () ); + assertTrue ( children.contains ( SimplePath.parse ( "/a/b/c/3" ) ) ); + + final Map childData = db.loadChildrenOf ( SimplePath.parse("/a/b/c") ); + assertEquals ( 10, childData.size () ); + assertTrue ( childData.containsKey ( SimplePath.parse ( "/a/b/c/3" ) ) ); + + final Map bChildData = db.loadChildrenOf ( SimplePath.parse("/a/b/") ); + assertEquals ( 1, bChildData.size () ); + assertTrue ( bChildData.containsKey ( SimplePath.parse ( "/a/b/c" ) ) ); + assertNull ( bChildData.get ( SimplePath.parse ( "/a/b/c" ) ) ); + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/SimplePathTest.java b/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/SimplePathTest.java new file mode 100644 index 0000000..cdd2adf --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/configs/confimpl/SimplePathTest.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.configs.confimpl; + +import junit.framework.TestCase; + +import org.junit.Test; + +public class SimplePathTest extends TestCase +{ + @Test + public void testParseSimple () + { + final SimplePath p = SimplePath.parse ( "/a/b/c" ); + assertEquals ( "c", p.getName () ); + assertEquals ( "b", p.getParent().getName () ); + assertEquals ( "a", p.getParent ().getParent().getName () ); + assertEquals ( "/a/b/c", p.toString () ); + } + + @Test + public void testRootName () + { + final SimplePath p = SimplePath.getRootPath (); + assertEquals ( "/", p.toString () ); + assertNull ( p.getParent () ); + } + + @Test + public void testNaming () + { + for ( String path : kPaths ) + { + final SimplePath p = SimplePath.parse ( path ); + assertEquals ( path, p.toString () ); + } + } + + @Test + public void testParseRootEquivalent () + { + final SimplePath p = SimplePath.parse ( "/" ); + assertEquals ( p, SimplePath.getRootPath () ); + } + + @Test + public void testTrailingSlashTrim () + { + final SimplePath p = SimplePath.parse ( "/a/b/" ); + assertEquals ( "/a/b", p.toString () ); + } + + private static final String[] kPaths = + { + "/", + "/a", + "/a/b" + }; +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapperTest.java b/saserverlibrary/src/test/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapperTest.java new file mode 100644 index 0000000..d1ca3c4 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/drumlin/till/nv/impl/nvInstallTypeWrapperTest.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.drumlin.till.nv.impl; + +import junit.framework.TestCase; + +import org.junit.Test; + +public class nvInstallTypeWrapperTest extends TestCase +{ + @Test + public void testParsing () + { + assertEquals ( "foo", nvInstallTypeWrapper.parse ( "foo" ) ); + assertEquals ( "foo", nvInstallTypeWrapper.parse ( "foo[sys]" ) ); + assertEquals ( "foo", nvInstallTypeWrapper.parse ( "foo[sys@user]" ) ); + assertEquals ( "foo", nvInstallTypeWrapper.parse ( "foo[@user]" ) ); + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclTest.java b/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclTest.java new file mode 100644 index 0000000..e580c26 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclTest.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; + +public class NsaAclTest extends TestCase +{ + @Test + public void testEmptyAndNull () throws AccessDeniedException + { + assertNull ( NsaAcl.fromJson ( (String)null, false ) ); + + NsaAcl acl = NsaAcl.fromJson ( (String)null, true ); + assertNotNull ( acl ); + assertEquals ( 0, acl.getUsers ().size() ); + + acl = NsaAcl.fromJson ( "", false ); + assertNotNull ( acl ); + assertEquals ( 0, acl.getUsers ().size() ); + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclUtilsTest.java b/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclUtilsTest.java new file mode 100644 index 0000000..066596f --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/security/NsaAclUtilsTest.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.security.ReadWriteSecuredResource.AccessDeniedException; + +public class NsaAclUtilsTest extends TestCase +{ + @Test + public void testDefaultHandling () throws AccessDeniedException + { + final NsaAcl empty = new NsaAcl (); + NsaAclUtils.checkUserAccess ( "", empty, null ); + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiDbTest.java b/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiDbTest.java new file mode 100644 index 0000000..f15f779 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiDbTest.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.confimpl.MemConfigDb; +import com.att.nsa.security.db.BaseNsaApiDbImpl; +import com.att.nsa.security.db.NsaApiDb.KeyExistsException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKeyFactory; + +public class NsaApiDbTest extends TestCase +{ + @Test + public void testDbStorage () throws KeyExistsException, ConfigDbException + { + final MemConfigDb cdb = new MemConfigDb (); + final BaseNsaApiDbImpl apiDb = new BaseNsaApiDbImpl ( cdb, new NsaSimpleApiKeyFactory () ); + + { + final NsaSimpleApiKey key = apiDb.createApiKey ( "123", "456" ); + key.set ( "foo", "bar" ); + apiDb.saveApiKey ( key ); + } + + { + final NsaApiKey key = apiDb.loadApiKey ( "123" ); + assertEquals ( "bar", key.get ( "foo" ) ); + } + + { + apiDb.deleteApiKey ( "123" ); + assertNull ( apiDb.loadApiKey ( "123" ) ); + } + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiOnZkDbTest.java b/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiOnZkDbTest.java new file mode 100644 index 0000000..57b3649 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/security/NsaApiOnZkDbTest.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security; + +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import junit.framework.TestCase; + +import org.junit.Ignore; +import org.junit.Test; + +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.confimpl.EncryptingLayer; +import com.att.nsa.configs.confimpl.ZkConfigDb; +import com.att.nsa.security.db.BaseNsaApiDbImpl; +import com.att.nsa.security.db.EncryptingApiDbImpl; +import com.att.nsa.security.db.NsaApiDb.KeyExistsException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKeyFactory; + +@Ignore +public class NsaApiOnZkDbTest extends TestCase +{ + @Test + public void testDbStorage () throws KeyExistsException, ConfigDbException + { + final ZkConfigDb cdb = new ZkConfigDb ( "localhost", "/fe3c" ); + final BaseNsaApiDbImpl apiDb = new BaseNsaApiDbImpl ( cdb, new NsaSimpleApiKeyFactory () ); + + { + final NsaSimpleApiKey key = apiDb.createApiKey ( "123", "456" ); + key.set ( "foo", "bar" ); + apiDb.saveApiKey ( key ); + } + + { + final NsaSimpleApiKey key = apiDb.loadApiKey ( "123" ); + assertEquals ( "bar", key.get ( "foo" ) ); + } + + { + apiDb.deleteApiKey ( "123" ); + assertNull ( apiDb.loadApiKey ( "123" ) ); + } + } + + @Test + public void testDbEncStorage () throws KeyExistsException, ConfigDbException, NoSuchAlgorithmException + { + final byte[] kIv = new byte[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 }; + + final String base64Key = EncryptingLayer.createSecretKey (); + + final ZkConfigDb cdb = new ZkConfigDb ( "localhost", "/fe3c" ); + + final Key dbkey = EncryptingLayer.readSecretKey ( base64Key ); + final BaseNsaApiDbImpl apiDb = new EncryptingApiDbImpl ( cdb, new NsaSimpleApiKeyFactory (), dbkey, kIv ); + + { + final NsaSimpleApiKey key = apiDb.createApiKey ( "123", "456" ); + key.set ( "foo", "bar" ); + apiDb.saveApiKey ( key ); + } + + { + final NsaSimpleApiKey key = apiDb.loadApiKey ( "123" ); + assertEquals ( "bar", key.get ( "foo" ) ); + } + + { + apiDb.deleteApiKey ( "123" ); + assertNull ( apiDb.loadApiKey ( "123" ) ); + } + } +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/security/authenticators/OriginalUebAuthenticatorTest.java b/saserverlibrary/src/test/java/com/att/nsa/security/authenticators/OriginalUebAuthenticatorTest.java new file mode 100644 index 0000000..b30fd7f --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/security/authenticators/OriginalUebAuthenticatorTest.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.security.authenticators; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.configs.ConfigDbException; +import com.att.nsa.configs.confimpl.MemConfigDb; +import com.att.nsa.drumlin.till.data.sha1HmacSigner; +import com.att.nsa.security.NsaApiKey; +import com.att.nsa.security.db.BaseNsaApiDbImpl; +import com.att.nsa.security.db.NsaApiDb.KeyExistsException; +import com.att.nsa.security.db.simple.NsaSimpleApiKey; +import com.att.nsa.security.db.simple.NsaSimpleApiKeyFactory; + +public class OriginalUebAuthenticatorTest extends TestCase +{ + @Test + public void testAuth () throws KeyExistsException, ConfigDbException + { + final MemConfigDb cdb = new MemConfigDb (); + final BaseNsaApiDbImpl apiDb = new BaseNsaApiDbImpl ( cdb, new NsaSimpleApiKeyFactory () ); + + { + final NsaSimpleApiKey key = apiDb.createApiKey ( "123", "456" ); + apiDb.saveApiKey ( key ); + } + + final OriginalUebAuthenticator mgr = new OriginalUebAuthenticator ( apiDb, 1000*60*10 ); + + NsaApiKey key = mgr.authenticate ( "localhost", null, "badXDate", "badDate", null ); + assertNull ( "no xauth", key ); + + key = mgr.authenticate ( "localhost", "badXauth", "badXDate", "badDate", null ); + assertNull ( "bad xauth", key ); + + key = mgr.authenticate ( "localhost", "nosuch:key", "badXDate", "badDate", null ); + assertNull ( "bad date", key ); + + key = mgr.authenticate ( "localhost", "nosuch:key", sdf.format ( new Date() ), null, null ); + assertNull ( "no api key", key ); + + key = mgr.authenticate ( "localhost", "123:xxx", sdf.format ( new Date() ), null, null ); + assertNull ( "bad signature", key ); + + final String xDate = sdf.format(new Date()); + final String sig = sha1HmacSigner.sign ( xDate, "456" ); + key = mgr.authenticate ( "localhost", "123:" + sig, xDate, null, null ); + assertNotNull ( key ); + } + + @Test + public void testDateParse () throws KeyExistsException, ConfigDbException + { + assertEquals ( 1434552360000L, parseDate ( "2015-06-17T10:46:00-0400" ) ); + assertEquals ( 1434552360000L, parseDate ( "2015-06-17T10:46:00-04:00" ) ); + } + + private long parseDate ( String s ) + { + final Date d = OriginalUebAuthenticator.getClientDate ( s ); + return d == null ? -1 : d.getTime (); + } + + private static final SimpleDateFormat sdf = new SimpleDateFormat ( "yyyy-MM-dd'T'HH:mm:ssz" ); +} diff --git a/saserverlibrary/src/test/java/com/att/nsa/timedata/impl/mem/MemTsDbTest.java b/saserverlibrary/src/test/java/com/att/nsa/timedata/impl/mem/MemTsDbTest.java new file mode 100644 index 0000000..243dcf6 --- /dev/null +++ b/saserverlibrary/src/test/java/com/att/nsa/timedata/impl/mem/MemTsDbTest.java @@ -0,0 +1,66 @@ +/******************************************************************************* + * Copyright (c) 2016 AT&T Intellectual Property. All rights reserved. + *******************************************************************************/ +package com.att.nsa.timedata.impl.mem; + +import java.util.Iterator; +import java.util.List; + +import junit.framework.TestCase; + +import org.junit.Test; + +import com.att.nsa.timedata.TimeSeriesEntry; + +public class MemTsDbTest extends TestCase +{ + @Test + public void testBasicPutGet () + { + final MemTsDb db = new MemTsDb (); + db.put ( "foo", 1, "1" ); + db.put ( "foo", 2, "2" ); + db.put ( "foo", 3, "3" ); + final TimeSeriesEntry val = db.get ( "foo", 2 ); + assertNotNull ( val ); + assertEquals ( "2", val.getValue () ); + } + + @Test + public void testRangeGetWithExactMatches () + { + final MemTsDb db = new MemTsDb (); + db.put ( "foo", 1, "1" ); + db.put ( "foo", 2, "2" ); + db.put ( "foo", 3, "3" ); + db.put ( "foo", 5, "5" ); + db.put ( "foo", 6, "6" ); + db.put ( "foo", 8, "8" ); + final List> range = db.get ( "foo", 2, 6 ); + assertNotNull ( range ); + assertEquals ( 4, range.size () ); + } + + @Test + public void testRangeGetWithoutMatches () + { + final MemTsDb db = new MemTsDb (); + db.put ( "foo", 1, "1" ); + db.put ( "foo", 2, "2" ); + db.put ( "foo", 3, "3" ); + db.put ( "foo", 5, "5" ); + db.put ( "foo", 6, "6" ); + db.put ( "foo", 8, "8" ); + final List> range = db.get ( "foo", 4, 10 ); + assertNotNull ( range ); + assertEquals ( 3, range.size () ); + final Iterator> it = range.iterator (); + assertTrue ( it.hasNext () ); + assertEquals ( "5", it.next ().getValue () ); + assertTrue ( it.hasNext () ); + assertEquals ( "6", it.next ().getValue () ); + assertTrue ( it.hasNext () ); + assertEquals ( "8", it.next ().getValue () ); + assertFalse ( it.hasNext () ); + } +} diff --git a/saserverlibrary/src/test/resources/keystore.dummy b/saserverlibrary/src/test/resources/keystore.dummy new file mode 100644 index 0000000..c82f9e4 Binary files /dev/null and b/saserverlibrary/src/test/resources/keystore.dummy differ