Permalink
Browse files

checking in source after creating git repo

  • Loading branch information...
1 parent a061739 commit 71f50e156cb0962b2d4228fc3a4a423a4c93a6c7 @benlucchesi committed Nov 2, 2012
Showing with 1,783 additions and 0 deletions.
  1. +17 −0 .gitignore
  2. +68 −0 CookieSessionV2GrailsPlugin.groovy
  3. +5 −0 application.properties
  4. +26 −0 grails-app/conf/BuildConfig.groovy
  5. +20 −0 grails-app/conf/Config.groovy
  6. +43 −0 grails-app/conf/DataSource.groovy
  7. +13 −0 grails-app/conf/UrlMappings.groovy
  8. 0 grails-app/i18n/messages.properties
  9. +336 −0 src/groovy/com/granicus/grails/plugins/cookiesession/CookieSessionRepository.groovy
  10. +29 −0 src/groovy/com/granicus/grails/plugins/cookiesession/SessionRepository.groovy
  11. +46 −0 src/java/com/granicus/grails/plugins/cookiesession/CookieSessionFilter.java
  12. +154 −0 src/java/com/granicus/grails/plugins/cookiesession/SerializableSession.java
  13. +84 −0 src/java/com/granicus/grails/plugins/cookiesession/SessionRepositoryRequestWrapper.java
  14. +99 −0 src/java/com/granicus/grails/plugins/cookiesession/SessionRepositoryResponseWrapper.java
  15. +33 −0 web-app/WEB-INF/applicationContext.xml
  16. +14 −0 web-app/WEB-INF/sitemesh.xml
  17. +109 −0 web-app/css/errors.css
  18. +596 −0 web-app/css/main.css
  19. +82 −0 web-app/css/mobile.css
  20. BIN web-app/images/apple-touch-icon-retina.png
  21. BIN web-app/images/apple-touch-icon.png
  22. BIN web-app/images/favicon.ico
  23. BIN web-app/images/grails_logo.jpg
  24. BIN web-app/images/grails_logo.png
  25. BIN web-app/images/leftnav_btm.png
  26. BIN web-app/images/leftnav_midstretch.png
  27. BIN web-app/images/leftnav_top.png
  28. BIN web-app/images/skin/database_add.png
  29. BIN web-app/images/skin/database_delete.png
  30. BIN web-app/images/skin/database_edit.png
  31. BIN web-app/images/skin/database_save.png
  32. BIN web-app/images/skin/database_table.png
  33. BIN web-app/images/skin/exclamation.png
  34. BIN web-app/images/skin/house.png
  35. BIN web-app/images/skin/information.png
  36. BIN web-app/images/skin/shadow.jpg
  37. BIN web-app/images/skin/sorted_asc.gif
  38. BIN web-app/images/skin/sorted_desc.gif
  39. BIN web-app/images/spinner.gif
  40. BIN web-app/images/springsource.png
  41. +9 −0 web-app/js/application.js
View
17 .gitignore
@@ -0,0 +1,17 @@
+/.settings
+/cobertura.ser
+stacktrace.log
+/plugin.xml
+/target
+/out
+/web-app/WEB-INF/classes
+
+/*.zip
+/*.zip.sha1
+*.class
+/.idea
+*.iml
+*.ipr
+*.iws
+*.tld
+
View
68 CookieSessionV2GrailsPlugin.groovy
@@ -0,0 +1,68 @@
+import org.springframework.web.filter.DelegatingFilterProxy
+import grails.util.Environment
+import com.granicus.grails.plugins.cookiesession.CookieSessionFilter
+import com.granicus.grails.plugins.cookiesession.CookieSessionRepository
+import org.codehaus.groovy.grails.orm.hibernate.ConfigurableLocalSessionFactoryBean
+
+class CookieSessionV2GrailsPlugin {
+ def version = "0.1"
+ def grailsVersion = "2.1.0 > *"
+ def title = "Cookie Session V2" // Headline display name of the plugin
+ def author = "Ben Lucchesi"
+ def authorEmail = "benlucchesi@gmail.com"
+ def description = "The Cookie Session V2 plugin stores the session data in cookies to support the development of stateless web applications. This implementation works with flash scope and webflow."
+ def documentation = "http://github.com/benlucchesi/grails-cookie-session-v2"
+ def license = "APACHE"
+
+ // Online location of the plugin's browseable source code.
+ def scm = [url: 'https://github.com/benlucchesi/grails-cookie-session-v2']
+
+ def getWebXmlFilterOrder() {
+ // make sure the filter is first
+ [cookieSessionFilter: -100]
+ }
+
+ def doWithWebDescriptor = { xml ->
+
+ if ( !application.config.grails.plugin.cookiesession.enabled ) {
+ return
+ }
+
+ // add the filter after the last context-param
+ def contextParam = xml.'context-param'
+
+ contextParam[contextParam.size() - 1] + {
+ 'filter' {
+ 'filter-name'('cookieSessionFilter')
+ 'filter-class'(DelegatingFilterProxy.name)
+ }
+ }
+
+ def filter = xml.'filter'
+ filter[filter.size() - 1] + {
+ 'filter-mapping' {
+ 'filter-name'('cookieSessionFilter')
+ 'url-pattern'('/*')
+ }
+ }
+
+ println xml
+ }
+
+ def doWithSpring = {
+
+ if ( !application.config.grails.plugin.cookiesession.enabled ) {
+ return
+ }
+
+ sessionRepository(CookieSessionRepository){
+ grailsApplication = ref("grailsApplication")
+ }
+
+ cookieSessionFilter(CookieSessionFilter) {
+ sessionRepository = ref("sessionRepository")
+ }
+
+ }
+
+}
View
5 application.properties
@@ -0,0 +1,5 @@
+#Grails Metadata file
+#Thu Nov 01 17:17:23 PDT 2012
+app.grails.version=2.1.0
+app.name=cookie-session-v2
+plugins.tomcat=2.1.0
View
26 grails-app/conf/BuildConfig.groovy
@@ -0,0 +1,26 @@
+grails.project.class.dir = "target/classes"
+grails.project.test.class.dir = "target/test-classes"
+grails.project.test.reports.dir = "target/test-reports"
+grails.project.target.level = 1.6
+//grails.project.war.file = "target/${appName}-${appVersion}.war"
+
+grails.project.dependency.resolution = {
+ // inherit Grails' default dependencies
+ inherits("global") {
+ }
+ log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
+ repositories {
+ grailsCentral()
+ }
+ dependencies {
+ }
+
+ plugins {
+
+ build(":svn:1.0.2", ":release:2.0.4") {
+ export = false
+ }
+
+ compile(":webxml:1.4.1")
+ }
+}
View
20 grails-app/conf/Config.groovy
@@ -0,0 +1,20 @@
+// configuration for plugin testing - will not be included in the plugin zip
+
+log4j = {
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
+}
+
+grails.views.default.codec="none" // none, html, base64
+grails.views.gsp.encoding="UTF-8"
View
43 grails-app/conf/DataSource.groovy
@@ -0,0 +1,43 @@
+dataSource {
+ pooled = true
+ driverClassName = "org.h2.Driver"
+ username = "sa"
+ password = ""
+}
+hibernate {
+ cache.use_second_level_cache = true
+ cache.use_query_cache = true
+ cache.region.factory_class = 'net.sf.ehcache.hibernate.EhCacheRegionFactory'
+}
+// environment specific settings
+environments {
+ development {
+ dataSource {
+ dbCreate = "create-drop" // one of 'create', 'create-drop', 'update', 'validate', ''
+ url = "jdbc:h2:mem:devDb;MVCC=TRUE"
+ }
+ }
+ test {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:h2:mem:testDb;MVCC=TRUE"
+ }
+ }
+ production {
+ dataSource {
+ dbCreate = "update"
+ url = "jdbc:h2:prodDb;MVCC=TRUE"
+ pooled = true
+ properties {
+ maxActive = -1
+ minEvictableIdleTimeMillis=1800000
+ timeBetweenEvictionRunsMillis=1800000
+ numTestsPerEvictionRun=3
+ testOnBorrow=true
+ testWhileIdle=true
+ testOnReturn=true
+ validationQuery="SELECT 1"
+ }
+ }
+ }
+}
View
13 grails-app/conf/UrlMappings.groovy
@@ -0,0 +1,13 @@
+class UrlMappings {
+
+ static mappings = {
+ "/$controller/$action?/$id?"{
+ constraints {
+ // apply constraints here
+ }
+ }
+
+ "/"(view:"/index")
+ "500"(view:'/error')
+ }
+}
View
0 grails-app/i18n/messages.properties
No changes.
View
336 src/groovy/com/granicus/grails/plugins/cookiesession/CookieSessionRepository.groovy
@@ -0,0 +1,336 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Ben Lucchesi
+ * ben@granicus.com or benlucchesi@gmail.com
+ */
+
+package com.granicus.grails.plugins.cookiesession;
+
+import org.springframework.beans.factory.InitializingBean
+
+import java.io.ByteArrayOutputStream;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.Cookie;
+
+import java.util.zip.GZIPOutputStream
+import java.util.zip.GZIPInputStream
+
+import javax.crypto.spec.SecretKeySpec
+import javax.crypto.CipherInputStream
+import javax.crypto.CipherOutputStream
+import javax.crypto.SealedObject
+import javax.crypto.Cipher
+
+import org.codehaus.groovy.grails.commons.ConfigurationHolder as ch
+
+import java.util.UUID
+
+import groovy.util.logging.Log4j
+
+@Log4j
+class CookieSessionRepository implements SessionRepository, InitializingBean {
+
+ def grailsApplication
+
+ SecretKeySpec cryptoKey
+
+ String cookieName = "grails_session" // default cookie name
+ boolean encryptCookie = true
+ String cryptoAlgorithm = "Blowfish"
+ def cryptoSecret = null
+ long maxInactiveInterval = 60 * 60
+ int cookieCount = 8
+ int maxCookieSize = 2000
+
+ void afterPropertiesSet(){
+
+ log.trace "afterPropertiesSet()"
+
+ log.info "configuring CookieSessionRepository"
+
+ if( ch.config.grails.plugin.cookiesession.encryptcookie != null ){
+ encryptCookie = ch.config.grails.plugin.cookiesession.encryptcookie?true:false
+ log.info "grails.plugin.cookiesession.encryptcookie set: \'${encryptCookie}\'"
+ }
+ else{
+ encryptCookie = true
+ log.info "grails.plugin.cookiesession.encryptcookie not set. defaulting to \'${encryptCookie}\'"
+ }
+
+ if( ch.config.grails.plugin.cookiesession.cryptoalgorithm ){
+ cryptoAlgorithm = ch.config.grails.plugin.cookiesession.cryptoalgorithm.toString()
+ log.info "grails.plugin.cookiesession.cryptoalgorithm set: \'${cryptoAlgorithm}\'"
+ }
+ else{
+ cryptoAlgorithm = "Blowfish"
+ log.info "grails.plugin.cookiesession.cryptoalgorithm not set. defaulting to \'${cryptoAlgorithm}\'"
+ }
+
+ if( ch.config.grails.plugin.cookiesession.sessiontimeout ){
+ maxInactiveInterval = ch.config.grails.plugin.cookiesession.sessiontimeout * 1000
+ if( maxInactiveInterval < 0 ){
+ log.warn "config.grails.plugin.cookiesession.sessiontimeout needs to be greater than or equal to zero. defaulting to 0"
+ maxInactiveInterval = 0
+ }
+ log.info "grails.plugin.cookiesession.sessiontimeout set: ${maxInactiveInterval} ms."
+ }else{
+ maxInactiveInterval = 0
+ log.info "grails.plugin.cookiesession.sessiontimeout not set. defaulting to ${maxInactiveInterval} ms."
+ }
+
+ if( ch.config.grails.plugin.cookiesession.cookiename ){
+ cookieName = ch.config.grails.plugin.cookiesession.cookiename
+ log.info "grails.plugin.cookiesession.cookiename set: \'${cookieName}\'"
+ }else{
+ cookieName = "grails_session"
+ log.info "grails.plugin.cookiesession.cookiename not set. defaulting to \'${cookieName}\'"
+ }
+
+ if( ch.config.grails.plugin.cookiesession.secret ){
+ cryptoSecret = ch.config.grails.plugin.cookiesession.secret
+ log.info "grails.plugin.cookiesession.secret set: \'${cryptoSecret.collect{ 'x' }.join()}\'"
+ }else{
+ cryptoSecret = (0..4).collect{ UUID.randomUUID().toString() }.join()
+ log.info "grails.plugin.cookiesession.secret not set: defaulting to \'${cryptoSecret.collect{ 'x' }.join()}\'"
+ log.warn "Crypto secret is not configured for session repository. Sessions can only be decrypted for this instance of the application. to make session transportable between multiple instances of this application, set the grails.plugin.cookiesession.secret configuration explicitly."
+ }
+
+ if( ch.config.grails.plugin.cookiesession.cookiecount ){
+ cookieCount = ch.config.grails.plugin.cookiesession.cookiecount
+ log.info "grails.plugin.cookiesession.cookiecount set: ${cookieCount}"
+ }
+ else{
+ cookieCount = 3
+ log.info "grails.plugin.cookiesession.cookiecount not set. defaulting to ${cookieCount}"
+ }
+
+ if( ch.config.grails.plugin.cookiesession.maxcookiesize ){
+ maxCookieSize = ch.config.grails.plugin.cookiesession.maxcookiesize.toInteger()
+
+ if( maxCookieSize < 1024 && maxCookieSize > 4096 ){
+ maxCookieSize = 2048
+ log.info "grails.plugin.cookiesession.maxCookieSize must be between 1024 and 4096. defaulting to 2048"
+ }
+ else{
+ log.info "grails.plugin.cookiesession.maxCookieSize set: ${maxCookieSize}"
+ }
+ }
+ else{
+ maxCookieSize = 2048
+ log.info "grails.plugin.cookiesession.maxcookiesize no set. defaulting to ${maxCookieSize}"
+ }
+
+ if( maxCookieSize * cookieCount > 6114 ){
+ log.warn "the maxcookiesize and cookiecount settings will allow for a max session size of ${maxCookieSize*cookieCount} bytes. Make sure you increase the max http header size in order to support this configuration. see the help file for this plugin for instructions."
+ }
+
+ // initialize the crypto key
+ cryptoKey = new SecretKeySpec(cryptoSecret,cryptoAlgorithm.split('/')[0])
+ }
+
+ SerializableSession restoreSession( HttpServletRequest request ){
+ log.trace "restoreSession()"
+
+ SerializableSession session = null
+
+ // - get the data from the cookie
+ // - deserialize the session (handles compression and encryption)
+ // - check to see if the session is expired
+ // - return the session
+
+ def serializedSession = getDataFromCookie(request)
+
+ if( serializedSession ){
+ session = deserializeSession(serializedSession)
+ }
+
+ def currentTime = System.currentTimeMillis()
+ def lastAccessedTime = session?.lastAccessedTime?:0
+ long inactiveInterval = currentTime - lastAccessedTime
+
+ if( session && maxInactiveInterval == 0 ){
+ log.info "retrieved valid session from cookie. lastAccessedTime: ${new Date(lastAccessedTime)}"
+ session.isNewSession = false
+ session.lastAccessedTime = System.currentTimeMillis()
+ session.servletContext = request.servletContext
+ }
+ else if( session && inactiveInterval > maxInactiveInterval ){
+ log.info "retrieved expired session from cookie. lastAccessedTime: ${new Date(lastAccessedTime)}. expired by ${inactiveInterval} ms.";
+ session = null
+ }
+ else{
+ log.info "no session retrieved from cookie."
+ }
+
+ return session
+ }
+
+ void saveSession( SerializableSession session, HttpServletResponse response ){
+ log.trace "saveSession()"
+
+ String serializedSession = serializeSession(session)
+ putDataInCookie(response, serializedSession );
+ }
+
+ String serializeSession( SerializableSession session ){
+ log.trace "serializeSession()"
+
+ log.trace "serializing and compressing session"
+ ByteArrayOutputStream stream = new ByteArrayOutputStream()
+ new GZIPOutputStream(stream).withObjectOutputStream{ oos ->
+ oos.writeObject(session)
+ }
+
+ byte[] output = null
+ if( encryptCookie ){
+ log.trace "encrypting serialized session"
+ Cipher cipher = Cipher.getInstance(cryptoAlgorithm)
+ cipher.init( Cipher.ENCRYPT_MODE, cryptoKey )
+ output = cipher.doFinal(stream.toByteArray())
+ }
+ else{
+ output = stream.toByteArray()
+ }
+
+ log.trace "base64 encoding serialized session"
+ def serializedSession = output.encodeBase64().toString()
+
+ return serializedSession
+ }
+
+ SerializableSession deserializeSession( String serializedSession ){
+ log.trace "deserializeSession()"
+
+ def session = null
+
+ try
+ {
+ log.trace "decodeBase64 serialized session"
+ def input = serializedSession.decodeBase64()
+
+ if( encryptCookie ){
+ log.trace "decrypting cookie"
+ Cipher cipher = Cipher.getInstance(cryptoAlgorithm)
+ cipher.init( Cipher.DECRYPT_MODE, cryptoKey )
+ input = cipher.doFinal(input)
+ }
+
+ log.trace "decompressing and deserializing session"
+ def inputStream = new GZIPInputStream( new ByteArrayInputStream( input ) )
+ def objectInputStream = new ObjectInputStream(inputStream){
+ @Override
+ public Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
+ //noinspection GroovyUnusedCatchParameter
+ try {
+ return grailsApplication.classLoader.loadClass(desc.getName())
+ } catch (ClassNotFoundException ex) {
+ return Class.forName(desc.getName())
+ }
+ }
+ }
+
+ session = (SerializableSession)objectInputStream.readObject();
+
+ /*
+ .withObjectInputStream( grailsApplication.classLoader ){ ois ->
+ session = (SerializableSession)ois.readObject()
+ }
+ */
+ }
+ catch( excp ){
+ log.error "An error occurred while deserializing a session. ${excp}}"
+ session = null
+ }
+
+ log.debug "deserialized session: ${session != null}"
+
+ return session
+ }
+
+ private String[] splitString(String input){
+ log.trace "splitString()"
+
+ def list = new String[cookieCount];
+
+ if( !input ){
+ log.trace "input empty or null."
+ return list
+ }
+
+ def partitions = input.size() / maxCookieSize
+ log.trace "splitting input of size ${input.size()} string into ${partitions} paritions"
+
+ //for( int i = 0; i < partitions; i++ ){
+ (0..partitions).each{ i ->
+ def start = i * maxCookieSize;
+ def end = start + maxCookieSize - 1
+ if( end >= input.size() )
+ end = start + input.size() % maxCookieSize - 1
+ log.trace "partition: ${i}, start: ${start}, end: ${end}"
+ list[i] = input[start..end]
+ }
+
+ return list
+ }
+
+ private String combineStrings(def input){
+ log.trace "combineStrings()"
+ def output = input.join()
+ log.trace "combined ${input.size()} strings into output of length ${output.size()}."
+ return output
+ }
+
+ String getDataFromCookie(HttpServletRequest request){
+ log.trace "getDataFromCookie()"
+
+ def values = request.cookies.findAll{ it.name.startsWith(cookieName) }?.sort{ it.name.split('-')[1].toInteger() }.collect{ it.value }
+
+ def data = combineStrings(values)
+ log.debug "retrieved ${data.size()} bytes of data from ${values.size()} session cookies."
+
+ return data
+ }
+
+ void putDataInCookie(HttpServletResponse response, String value){
+ log.trace "putDataInCookie() - ${value.size()}"
+
+ def partitions = splitString(value)
+ partitions.eachWithIndex{ it, i ->
+ Cookie c = new Cookie( "${cookieName}-${i}".toString(), it?:'')
+ c.setSecure(false)
+ c.setPath("/")
+ response.addCookie(c)
+ log.trace "added ${cookieName}-${i} to response"
+ }
+
+ log.debug "added ${partitions.size()} session cookies to response."
+ }
+
+ void deleteCookie(HttpServletResponse response){
+ log.trace "deleteCookie()"
+ Cookie c = new Cookie( cookieName, "" )
+ c.path = "/"
+ c.maxAge = 0
+ c.setVersion( 0 )
+ }
+
+ boolean isSessionIdValid(String sessionId){
+ log.trace "isSessionIdValid() : ${sessionId}"
+ return true;
+ }
+}
View
29 src/groovy/com/granicus/grails/plugins/cookiesession/SessionRepository.groovy
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Ben Lucchesi
+ * ben@granicus.com or benlucchesi@gmail.com
+ */
+
+package com.granicus.grails.plugins.cookiesession;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletRequest;
+
+interface SessionRepository {
+ SerializableSession restoreSession( HttpServletRequest request )
+ void saveSession( SerializableSession session, HttpServletResponse response )
+ boolean isSessionIdValid(String sessionId)
+}
View
46 src/java/com/granicus/grails/plugins/cookiesession/CookieSessionFilter.java
@@ -0,0 +1,46 @@
+package com.granicus.grails.plugins.cookiesession;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import org.springframework.web.filter.OncePerRequestFilter;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.log4j.Logger;
+
+public class CookieSessionFilter extends OncePerRequestFilter {
+
+ final static Logger log = Logger.getLogger(CookieSessionFilter.class.getName());
+ String sessionId = "gsession";
+
+ // dependency injected
+ private SessionRepository sessionRepository;
+ public void setSessionRepository(SessionRepository repository){
+ sessionRepository = repository;
+ }
+ public SessionRepository getSessionRepository(){
+ return ( sessionRepository );
+ }
+
+ @Override
+ protected void initFilterBean() {
+ }
+
+ @Override
+ protected void doFilterInternal( HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain chain) throws IOException, ServletException {
+
+ if( log.isTraceEnabled() ){ log.trace("doFilterInteral()"); }
+
+ SessionRepositoryRequestWrapper requestWrapper = new SessionRepositoryRequestWrapper( request, sessionRepository );
+ requestWrapper.restoreSession();
+
+ SerializableSession session = (SerializableSession)requestWrapper.getSession();
+
+ SessionRepositoryResponseWrapper responseWrapper = new SessionRepositoryResponseWrapper( response, sessionRepository, session );
+ chain.doFilter(requestWrapper, responseWrapper);
+ }
+}
View
154 src/java/com/granicus/grails/plugins/cookiesession/SerializableSession.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Ben Lucchesi
+ * ben@granicus.com or benlucchesi@gmail.com
+ */
+
+package com.granicus.grails.plugins.cookiesession;
+
+import javax.servlet.http.HttpSession;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpSessionContext;
+
+import java.io.Serializable;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import java.util.HashMap;
+
+public class SerializableSession implements HttpSession, Serializable {
+
+ // borrowed from other cookie session plugin to fake a session context
+ @SuppressWarnings("deprecation") /* ServletAPI */
+ transient private final javax.servlet.http.HttpSessionContext SESSION_CONTEXT = new javax.servlet.http.HttpSessionContext() {
+ public HttpSession getSession(String sessionId) {
+ return null;
+ }
+ public Enumeration<String> getIds() {
+ return SESSION_CONTEXT_ID_ENUM;
+ }
+ };
+
+ transient private final Enumeration<String> SESSION_CONTEXT_ID_ENUM = new Enumeration<String>() {
+ public String nextElement() {
+ return null;
+ }
+ public boolean hasMoreElements() {
+ return false;
+ }
+ };
+
+ public final long serialVersionUID = 42L;
+ private long creationTime = 0;
+ private long lastAccessedTime = 0;
+ private HashMap<String,Serializable> attributes;
+
+ transient private ServletContext servletContext;
+ transient private boolean newSession;
+ transient private int maxInactiveInterval;
+
+ public SerializableSession(){
+ this.creationTime = System.currentTimeMillis();
+ this.lastAccessedTime = this.creationTime;
+ this.attributes = new HashMap<String,Serializable>();
+ }
+
+ public long getCreationTime(){
+ return ( creationTime );
+ }
+
+ public String getId(){
+ return ( "simplesession" );
+ }
+
+ public void setLastAccessedTime(long lastAccessedTime){
+ this.lastAccessedTime = lastAccessedTime;
+ }
+
+ public long getLastAccessedTime(){
+ return ( lastAccessedTime );
+ }
+
+ public ServletContext getServletContext(){
+ return ( servletContext );
+ }
+
+ public void setServletContext(ServletContext servletContext){
+ this.servletContext = servletContext;
+ }
+
+ public void setMaxInactiveInterval(int interval){
+ maxInactiveInterval = interval;
+ }
+
+ public int getMaxInactiveInterval(){
+ return ( maxInactiveInterval );
+ }
+
+ public HttpSessionContext getSessionContext(){
+ return SESSION_CONTEXT;
+ }
+
+ public Object getAttribute(String name){
+ return ( attributes.get(name) );
+ }
+
+ public Object getValue(String name){
+ return getAttribute(name);
+ }
+
+ public Enumeration getAttributeNames(){
+ System.out.println("getAttributeNames()");
+ final Iterator<String> keys = attributes.keySet().iterator();
+ final Enumeration names = new Enumeration(){
+ public boolean hasMoreElements(){ return keys.hasNext(); }
+ public Object nextElement(){ return keys.next(); }
+ };
+
+ return ( names );
+ }
+
+ public String[] getValueNames(){
+ return ( attributes.keySet().toArray( new String[0] ) );
+ }
+
+ public void setAttribute(String name, Object value){
+ attributes.put(name,(Serializable)value);
+ }
+
+ public void putValue(String name, Object value){
+ attributes.put(name,(Serializable)value);
+ }
+
+ public void removeAttribute(String name){
+ attributes.remove(name);
+ }
+
+ public void removeValue(String name){
+ attributes.remove(name);
+ }
+
+ public void invalidate(){
+ //TODO: store flag indicating that session is invalid
+ }
+
+ protected void setIsNewSession( boolean isNewSession ){
+ this.newSession = isNewSession;
+ }
+ public boolean isNew(){
+ return ( this.newSession );
+ }
+ }
View
84 src/java/com/granicus/grails/plugins/cookiesession/SessionRepositoryRequestWrapper.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Ben Lucchesi
+ * ben@granicus.com or benlucchesi@gmail.com
+ */
+
+package com.granicus.grails.plugins.cookiesession;
+
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.log4j.Logger;
+
+public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
+
+ final static Logger log = Logger.getLogger(SessionRepositoryRequestWrapper.class.getName());
+
+ private SerializableSession session;
+ private SessionRepository sessionRepository;
+
+ public SessionRepositoryRequestWrapper( HttpServletRequest request, SessionRepository sessionRepository ){
+ super( request );
+ this.sessionRepository = sessionRepository;
+ }
+
+ public void restoreSession() {
+ if( log.isTraceEnabled() ){ log.trace("restoreSession()"); }
+
+ // use the sessionRepository to attempt to retrieve session object
+ // if a session was restored
+ // - set isNew == false
+ // - assign the servlet context
+
+ session = sessionRepository.restoreSession( this );
+ }
+
+ @Override
+ public HttpSession getSession(boolean create){
+
+ // if there isn't an existing session object and create == true, then
+ // - create a new session object
+ // - set isNew == true
+ // - assign the servlet context
+ // else
+ // - return the session field regardless if what it contains
+
+ if( session == null && create == true ){
+ session = new SerializableSession();
+ session.setIsNewSession( true );
+ session.setServletContext( this.getServletContext() );
+ }
+
+ return ( session );
+ }
+
+ @Override
+ public HttpSession getSession(){
+ if( log.isTraceEnabled() ){ log.trace("getSession()"); }
+
+ return( this.getSession(true) );
+ }
+
+ @Override
+ public boolean isRequestedSessionIdValid(){
+ if( log.isTraceEnabled() ){ log.trace("isRequestedSessionIdValid()"); }
+
+ // session repository is responsible for determining if the requested session id is valid.
+ return sessionRepository.isSessionIdValid( this.getRequestedSessionId() );
+ }
+ }
View
99 src/java/com/granicus/grails/plugins/cookiesession/SessionRepositoryResponseWrapper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Ben Lucchesi
+ * ben@granicus.com or benlucchesi@gmail.com
+ */
+
+
+package com.granicus.grails.plugins.cookiesession;
+
+import javax.servlet.http.HttpServletResponseWrapper;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletOutputStream;
+
+import org.apache.log4j.Logger;
+
+public class SessionRepositoryResponseWrapper extends HttpServletResponseWrapper {
+
+ static final Logger log = Logger.getLogger(SessionRepositoryResponseWrapper.class.getName());
+
+ private String sessionId = "simplesession";
+ private SessionRepository sessionRepository;
+ private SerializableSession session;
+ private boolean sessionSaved = false;
+
+ public SessionRepositoryResponseWrapper( HttpServletResponse response,
+ SessionRepository sessionRepository, SerializableSession session) {
+ super(response);
+ this.sessionRepository = sessionRepository;
+ this.session = session;
+ }
+
+ public void saveSession(){
+
+ if( log.isTraceEnabled() ){ log.trace("saveSession()"); }
+
+ if( this.isCommitted() ){
+ if( log.isTraceEnabled() ){ log.trace("response is already committed, not attempting to save."); }
+ return;
+ }
+
+ if( sessionSaved == true ){
+ if( log.isTraceEnabled() ){ log.trace("session is already saved, not attempting to save again."); }
+ return;
+ }
+
+ if( session == null ){
+ if( log.isTraceEnabled() ){ log.trace("session is null, not saving."); }
+ return;
+ }
+
+ // flag the session as saved.
+ sessionSaved = true;
+
+ if( log.isTraceEnabled() ){ log.trace("calling session repository to save session."); }
+
+ sessionRepository.saveSession(session,this);
+ }
+
+ @Override
+ public void flushBuffer() throws java.io.IOException{
+ if( log.isTraceEnabled() ){ log.trace("intercepting flushBuffer to save session"); }
+ this.saveSession();
+ super.flushBuffer();
+ }
+
+ @Override
+ public java.io.PrintWriter getWriter() throws java.io.IOException{
+ if( log.isTraceEnabled() ){ log.trace("intercepting getWriter to save session"); }
+ this.saveSession();
+ return ( super.getWriter() );
+ }
+
+ @Override
+ public ServletOutputStream getOutputStream() throws java.io.IOException{
+ if( log.isTraceEnabled() ){ log.trace("intercepting getOutputStream to save session"); }
+ this.saveSession();
+ return ( super.getOutputStream() );
+ }
+
+ @Override
+ public void sendRedirect(String location) throws java.io.IOException{
+ if( log.isTraceEnabled() ){ log.trace("intercepting sendRedirect(" + location + ") to save session"); }
+ this.saveSession();
+ super.sendRedirect(location);
+ }
+}
View
33 web-app/WEB-INF/applicationContext.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
+
+ <bean id="grailsApplication" class="org.codehaus.groovy.grails.commons.GrailsApplicationFactoryBean">
+ <description>Grails application factory bean</description>
+ <property name="grailsDescriptor" value="/WEB-INF/grails.xml" />
+ <property name="grailsResourceLoader" ref="grailsResourceLoader" />
+ </bean>
+
+ <bean id="pluginManager" class="org.codehaus.groovy.grails.plugins.GrailsPluginManagerFactoryBean">
+ <description>A bean that manages Grails plugins</description>
+ <property name="grailsDescriptor" value="/WEB-INF/grails.xml" />
+ <property name="application" ref="grailsApplication" />
+ </bean>
+
+ <bean id="grailsConfigurator" class="org.codehaus.groovy.grails.commons.spring.GrailsRuntimeConfigurator">
+ <constructor-arg>
+ <ref bean="grailsApplication" />
+ </constructor-arg>
+ <property name="pluginManager" ref="pluginManager" />
+ </bean>
+
+ <bean id="grailsResourceLoader" class="org.codehaus.groovy.grails.commons.GrailsResourceLoaderFactoryBean" />
+
+ <bean id="characterEncodingFilter" class="org.springframework.web.filter.CharacterEncodingFilter">
+ <property name="encoding">
+ <value>utf-8</value>
+ </property>
+ </bean>
+</beans>
View
14 web-app/WEB-INF/sitemesh.xml
@@ -0,0 +1,14 @@
+<sitemesh>
+ <page-parsers>
+ <parser content-type="text/html"
+ class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+ <parser content-type="text/html;charset=ISO-8859-1"
+ class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+ <parser content-type="text/html;charset=UTF-8"
+ class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
+ </page-parsers>
+
+ <decorator-mappers>
+ <mapper class="org.codehaus.groovy.grails.web.sitemesh.GrailsLayoutDecoratorMapper" />
+ </decorator-mappers>
+</sitemesh>
View
109 web-app/css/errors.css
@@ -0,0 +1,109 @@
+h1, h2 {
+ margin: 10px 25px 5px;
+}
+
+h2 {
+ font-size: 1.1em;
+}
+
+.filename {
+ font-style: italic;
+}
+
+.exceptionMessage {
+ margin: 10px;
+ border: 1px solid #000;
+ padding: 5px;
+ background-color: #E9E9E9;
+}
+
+.stack,
+.snippet {
+ margin: 0 25px 10px;
+}
+
+.stack,
+.snippet {
+ border: 1px solid #ccc;
+ -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ box-shadow: 0 0 2px rgba(0,0,0,0.2);
+}
+
+/* error details */
+.error-details {
+ border-top: 1px solid #FFAAAA;
+ -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ border-bottom: 1px solid #FFAAAA;
+ -mox-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ -webkit-box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ box-shadow: 0 0 2px rgba(0,0,0,0.2);
+ background-color:#FFF3F3;
+ line-height: 1.5;
+ overflow: hidden;
+ padding: 5px;
+ padding-left:25px;
+}
+
+.error-details dt {
+ clear: left;
+ float: left;
+ font-weight: bold;
+ margin-right: 5px;
+}
+
+.error-details dt:after {
+ content: ":";
+}
+
+.error-details dd {
+ display: block;
+}
+
+/* stack trace */
+.stack {
+ padding: 5px;
+ overflow: auto;
+ height: 150px;
+}
+
+/* code snippet */
+.snippet {
+ background-color: #fff;
+ font-family: monospace;
+}
+
+.snippet .line {
+ display: block;
+}
+
+.snippet .lineNumber {
+ background-color: #ddd;
+ color: #999;
+ display: inline-block;
+ margin-right: 5px;
+ padding: 0 3px;
+ text-align: right;
+ width: 3em;
+}
+
+.snippet .error {
+ background-color: #fff3f3;
+ font-weight: bold;
+}
+
+.snippet .error .lineNumber {
+ background-color: #faa;
+ color: #333;
+ font-weight: bold;
+}
+
+.snippet .line:first-child .lineNumber {
+ padding-top: 5px;
+}
+
+.snippet .line:last-child .lineNumber {
+ padding-bottom: 5px;
+}
View
596 web-app/css/main.css
@@ -0,0 +1,596 @@
+/* FONT STACK */
+body,
+input, select, textarea {
+ font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ line-height: 1.1;
+}
+
+/* BASE LAYOUT */
+
+html {
+ background-color: #ddd;
+ background-image: -moz-linear-gradient(center top, #aaa, #ddd);
+ background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #aaa), color-stop(1, #ddd));
+ background-image: linear-gradient(top, #aaa, #ddd);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorStr = '#aaaaaa', EndColorStr = '#dddddd');
+ background-repeat: no-repeat;
+ height: 100%;
+ /* change the box model to exclude the padding from the calculation of 100% height (IE8+) */
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+html.no-cssgradients {
+ background-color: #aaa;
+}
+
+.ie6 html {
+ height: 100%;
+}
+
+html * {
+ margin: 0;
+}
+
+body {
+ background: #ffffff;
+ color: #333333;
+ margin: 0 auto;
+ max-width: 960px;
+ overflow-x: hidden; /* prevents box-shadow causing a horizontal scrollbar in firefox when viewport < 960px wide */
+ -moz-box-shadow: 0 0 0.3em #255b17;
+ -webkit-box-shadow: 0 0 0.3em #255b17;
+ box-shadow: 0 0 0.3em #255b17;
+}
+
+#grailsLogo {
+ background-color: #abbf78;
+}
+
+/* replace with .no-boxshadow body if you have modernizr available */
+.ie6 body,
+.ie7 body,
+.ie8 body {
+ border-color: #255b17;
+ border-style: solid;
+ border-width: 0 1px;
+}
+
+.ie6 body {
+ height: 100%;
+}
+
+a:link, a:visited, a:hover {
+ color: #48802c;
+}
+
+a:hover, a:active {
+ outline: none; /* prevents outline in webkit on active links but retains it for tab focus */
+}
+
+h1 {
+ color: #48802c;
+ font-weight: normal;
+ font-size: 1.25em;
+ margin: 0.8em 0 0.3em 0;
+}
+
+ul {
+ padding: 0;
+}
+
+img {
+ border: 0;
+}
+
+/* GENERAL */
+
+#grailsLogo a {
+ display: inline-block;
+ margin: 1em;
+}
+
+.content {
+}
+
+.content h1 {
+ border-bottom: 1px solid #CCCCCC;
+ margin: 0.8em 1em 0.3em;
+ padding: 0 0.25em;
+}
+
+.scaffold-list h1 {
+ border: none;
+}
+
+.footer {
+ background: #abbf78;
+ color: #000;
+ clear: both;
+ font-size: 0.8em;
+ margin-top: 1.5em;
+ padding: 1em;
+ min-height: 1em;
+}
+
+.footer a {
+ color: #255b17;
+}
+
+.spinner {
+ background: url(../images/spinner.gif) 50% 50% no-repeat transparent;
+ height: 16px;
+ width: 16px;
+ padding: 0.5em;
+ position: absolute;
+ right: 0;
+ top: 0;
+ text-indent: -9999px;
+}
+
+/* NAVIGATION MENU */
+
+.nav {
+ background-color: #efefef;
+ padding: 0.5em 0.75em;
+ -moz-box-shadow: 0 0 3px 1px #aaaaaa;
+ -webkit-box-shadow: 0 0 3px 1px #aaaaaa;
+ box-shadow: 0 0 3px 1px #aaaaaa;
+ zoom: 1;
+}
+
+.nav ul {
+ overflow: hidden;
+ padding-left: 0;
+ zoom: 1;
+}
+
+.nav li {
+ display: block;
+ float: left;
+ list-style-type: none;
+ margin-right: 0.5em;
+ padding: 0;
+}
+
+.nav a {
+ color: #666666;
+ display: block;
+ padding: 0.25em 0.7em;
+ text-decoration: none;
+ -moz-border-radius: 0.3em;
+ -webkit-border-radius: 0.3em;
+ border-radius: 0.3em;
+}
+
+.nav a:active, .nav a:visited {
+ color: #666666;
+}
+
+.nav a:focus, .nav a:hover {
+ background-color: #999999;
+ color: #ffffff;
+ outline: none;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+}
+
+.no-borderradius .nav a:focus, .no-borderradius .nav a:hover {
+ background-color: transparent;
+ color: #444444;
+ text-decoration: underline;
+}
+
+.nav a.home, .nav a.list, .nav a.create {
+ background-position: 0.7em center;
+ background-repeat: no-repeat;
+ text-indent: 25px;
+}
+
+.nav a.home {
+ background-image: url(../images/skin/house.png);
+}
+
+.nav a.list {
+ background-image: url(../images/skin/database_table.png);
+}
+
+.nav a.create {
+ background-image: url(../images/skin/database_add.png);
+}
+
+/* CREATE/EDIT FORMS AND SHOW PAGES */
+
+fieldset,
+.property-list {
+ margin: 0.6em 1.25em 0 1.25em;
+ padding: 0.3em 1.8em 1.25em;
+ position: relative;
+ zoom: 1;
+ border: none;
+}
+
+.property-list .fieldcontain {
+ list-style: none;
+ overflow: hidden;
+ zoom: 1;
+}
+
+.fieldcontain {
+ margin-top: 1em;
+}
+
+.fieldcontain label,
+.fieldcontain .property-label {
+ color: #666666;
+ text-align: right;
+ width: 25%;
+}
+
+.fieldcontain .property-label {
+ float: left;
+}
+
+.fieldcontain .property-value {
+ display: block;
+ margin-left: 27%;
+}
+
+label {
+ cursor: pointer;
+ display: inline-block;
+ margin: 0 0.25em 0 0;
+}
+
+input, select, textarea {
+ background-color: #fcfcfc;
+ border: 1px solid #cccccc;
+ font-size: 1em;
+ padding: 0.2em 0.4em;
+}
+
+select {
+ padding: 0.2em 0.2em 0.2em 0;
+}
+
+select[multiple] {
+ vertical-align: top;
+}
+
+textarea {
+ width: 250px;
+ height: 150px;
+ overflow: auto; /* IE always renders vertical scrollbar without this */
+ vertical-align: top;
+}
+
+input[type=checkbox], input[type=radio] {
+ background-color: transparent;
+ border: 0;
+ padding: 0;
+}
+
+input:focus, select:focus, textarea:focus {
+ background-color: #ffffff;
+ border: 1px solid #eeeeee;
+ outline: 0;
+ -moz-box-shadow: 0 0 0.5em #ffffff;
+ -webkit-box-shadow: 0 0 0.5em #ffffff;
+ box-shadow: 0 0 0.5em #ffffff;
+}
+
+.required-indicator {
+ color: #48802C;
+ display: inline-block;
+ font-weight: bold;
+ margin-left: 0.3em;
+ position: relative;
+ top: 0.1em;
+}
+
+ul.one-to-many {
+ display: inline-block;
+ list-style-position: inside;
+ vertical-align: top;
+}
+
+.ie6 ul.one-to-many, .ie7 ul.one-to-many {
+ display: inline;
+ zoom: 1;
+}
+
+ul.one-to-many li.add {
+ list-style-type: none;
+}
+
+/* EMBEDDED PROPERTIES */
+
+fieldset.embedded {
+ background-color: transparent;
+ border: 1px solid #CCCCCC;
+ margin-left: 0;
+ margin-right: 0;
+ padding-left: 0;
+ padding-right: 0;
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+fieldset.embedded legend {
+ margin: 0 1em;
+}
+
+/* MESSAGES AND ERRORS */
+
+.errors,
+.message {
+ font-size: 0.8em;
+ line-height: 2;
+ margin: 1em 2em;
+ padding: 0.25em;
+}
+
+.message {
+ background: #f3f3ff;
+ border: 1px solid #b2d1ff;
+ color: #006dba;
+ -moz-box-shadow: 0 0 0.25em #b2d1ff;
+ -webkit-box-shadow: 0 0 0.25em #b2d1ff;
+ box-shadow: 0 0 0.25em #b2d1ff;
+}
+
+.errors {
+ background: #fff3f3;
+ border: 1px solid #ffaaaa;
+ color: #cc0000;
+ -moz-box-shadow: 0 0 0.25em #ff8888;
+ -webkit-box-shadow: 0 0 0.25em #ff8888;
+ box-shadow: 0 0 0.25em #ff8888;
+}
+
+.errors ul,
+.message {
+ padding: 0;
+}
+
+.errors li {
+ list-style: none;
+ background: transparent url(../images/skin/exclamation.png) 0.5em 50% no-repeat;
+ text-indent: 2.2em;
+}
+
+.message {
+ background: transparent url(../images/skin/information.png) 0.5em 50% no-repeat;
+ text-indent: 2.2em;
+}
+
+/* form fields with errors */
+
+.error input, .error select, .error textarea {
+ background: #fff3f3;
+ border-color: #ffaaaa;
+ color: #cc0000;
+}
+
+.error input:focus, .error select:focus, .error textarea:focus {
+ -moz-box-shadow: 0 0 0.5em #ffaaaa;
+ -webkit-box-shadow: 0 0 0.5em #ffaaaa;
+ box-shadow: 0 0 0.5em #ffaaaa;
+}
+
+/* same effects for browsers that support HTML5 client-side validation (these have to be specified separately or IE will ignore the entire rule) */
+
+input:invalid, select:invalid, textarea:invalid {
+ background: #fff3f3;
+ border-color: #ffaaaa;
+ color: #cc0000;
+}
+
+input:invalid:focus, select:invalid:focus, textarea:invalid:focus {
+ -moz-box-shadow: 0 0 0.5em #ffaaaa;
+ -webkit-box-shadow: 0 0 0.5em #ffaaaa;
+ box-shadow: 0 0 0.5em #ffaaaa;
+}
+
+/* TABLES */
+
+table {
+ border-top: 1px solid #DFDFDF;
+ border-collapse: collapse;
+ width: 100%;
+ margin-bottom: 1em;
+}
+
+tr {
+ border: 0;
+}
+
+tr>td:first-child, tr>th:first-child {
+ padding-left: 1.25em;
+}
+
+tr>td:last-child, tr>th:last-child {
+ padding-right: 1.25em;
+}
+
+td, th {
+ line-height: 1.5em;
+ padding: 0.5em 0.6em;
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ background-color: #efefef;
+ background-image: -moz-linear-gradient(top, #ffffff, #eaeaea);
+ background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #ffffff), color-stop(1, #eaeaea));
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorStr = '#ffffff', EndColorStr = '#eaeaea');
+ -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#ffffff', EndColorStr='#eaeaea')";
+ color: #666666;
+ font-weight: bold;
+ line-height: 1.7em;
+ padding: 0.2em 0.6em;
+}
+
+thead th {
+ white-space: nowrap;
+}
+
+th a {
+ display: block;
+ text-decoration: none;
+}
+
+th a:link, th a:visited {
+ color: #666666;
+}
+
+th a:hover, th a:focus {
+ color: #333333;
+}
+
+th.sortable a {
+ background-position: right;
+ background-repeat: no-repeat;
+ padding-right: 1.1em;
+}
+
+th.asc a {
+ background-image: url(../images/skin/sorted_asc.gif);
+}
+
+th.desc a {
+ background-image: url(../images/skin/sorted_desc.gif);
+}
+
+.odd {
+ background: #f7f7f7;
+}
+
+.even {
+ background: #ffffff;
+}
+
+th:hover, tr:hover {
+ background: #E1F2B6;
+}
+
+/* PAGINATION */
+
+.pagination {
+ border-top: 0;
+ margin: 0;
+ padding: 0.3em 0.2em;
+ text-align: center;
+ -moz-box-shadow: 0 0 3px 1px #AAAAAA;
+ -webkit-box-shadow: 0 0 3px 1px #AAAAAA;
+ box-shadow: 0 0 3px 1px #AAAAAA;
+ background-color: #EFEFEF;
+}
+
+.pagination a,
+.pagination .currentStep {
+ color: #666666;
+ display: inline-block;
+ margin: 0 0.1em;
+ padding: 0.25em 0.7em;
+ text-decoration: none;
+ -moz-border-radius: 0.3em;
+ -webkit-border-radius: 0.3em;
+ border-radius: 0.3em;
+}
+
+.pagination a:hover, .pagination a:focus,
+.pagination .currentStep {
+ background-color: #999999;
+ color: #ffffff;
+ outline: none;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+}
+
+.no-borderradius .pagination a:hover, .no-borderradius .pagination a:focus,
+.no-borderradius .pagination .currentStep {
+ background-color: transparent;
+ color: #444444;
+ text-decoration: underline;
+}
+
+/* ACTION BUTTONS */
+
+.buttons {
+ background-color: #efefef;
+ overflow: hidden;
+ padding: 0.3em;
+ -moz-box-shadow: 0 0 3px 1px #aaaaaa;
+ -webkit-box-shadow: 0 0 3px 1px #aaaaaa;
+ box-shadow: 0 0 3px 1px #aaaaaa;
+ margin: 0.1em 0 0 0;
+ border: none;
+}
+
+.buttons input,
+.buttons a {
+ background-color: transparent;
+ border: 0;
+ color: #666666;
+ cursor: pointer;
+ display: inline-block;
+ margin: 0 0.25em 0;
+ overflow: visible;
+ padding: 0.25em 0.7em;
+ text-decoration: none;
+
+ -moz-border-radius: 0.3em;
+ -webkit-border-radius: 0.3em;
+ border-radius: 0.3em;
+}
+
+.buttons input:hover, .buttons input:focus,
+.buttons a:hover, .buttons a:focus {
+ background-color: #999999;
+ color: #ffffff;
+ outline: none;
+ text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.8);
+ -moz-box-shadow: none;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+
+.no-borderradius .buttons input:hover, .no-borderradius .buttons input:focus,
+.no-borderradius .buttons a:hover, .no-borderradius .buttons a:focus {
+ background-color: transparent;
+ color: #444444;
+ text-decoration: underline;
+}
+
+.buttons .delete, .buttons .edit, .buttons .save {
+ background-position: 0.7em center;
+ background-repeat: no-repeat;
+ text-indent: 25px;
+}
+
+.ie6 .buttons input.delete, .ie6 .buttons input.edit, .ie6 .buttons input.save,
+.ie7 .buttons input.delete, .ie7 .buttons input.edit, .ie7 .buttons input.save {
+ padding-left: 36px;
+}
+
+.buttons .delete {
+ background-image: url(../images/skin/database_delete.png);
+}
+
+.buttons .edit {
+ background-image: url(../images/skin/database_edit.png);
+}
+
+.buttons .save {
+ background-image: url(../images/skin/database_save.png);
+}
+
+a.skip {
+ position: absolute;
+ left: -9999px;
+}
View
82 web-app/css/mobile.css
@@ -0,0 +1,82 @@
+/* Styles for mobile devices */
+
+@media screen and (max-width: 480px) {
+ .nav {
+ padding: 0.5em;
+ }
+
+ .nav li {
+ margin: 0 0.5em 0 0;
+ padding: 0.25em;
+ }
+
+ /* Hide individual steps in pagination, just have next & previous */
+ .pagination .step, .pagination .currentStep {
+ display: none;
+ }
+
+ .pagination .prevLink {
+ float: left;
+ }
+
+ .pagination .nextLink {
+ float: right;
+ }
+
+ /* pagination needs to wrap around floated buttons */
+ .pagination {
+ overflow: hidden;
+ }
+
+ /* slightly smaller margin around content body */
+ fieldset,
+ .property-list {
+ padding: 0.3em 1em 1em;
+ }
+
+ input, textarea {
+ width: 100%;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+
+ select, input[type=checkbox], input[type=radio], input[type=submit], input[type=button], input[type=reset] {
+ width: auto;
+ }
+
+ /* hide all but the first column of list tables */
+ .scaffold-list td:not(:first-child),
+ .scaffold-list th:not(:first-child) {
+ display: none;
+ }
+
+ .scaffold-list thead th {
+ text-align: center;
+ }
+
+ /* stack form elements */
+ .fieldcontain {
+ margin-top: 0.6em;
+ }
+
+ .fieldcontain label,
+ .fieldcontain .property-label,
+ .fieldcontain .property-value {
+ display: block;
+ float: none;
+ margin: 0 0 0.25em 0;
+ text-align: left;
+ width: auto;
+ }
+
+ .errors ul,
+ .message p {
+ margin: 0.5em;
+ }
+
+ .error ul {
+ margin-left: 0;
+ }
+}
View
BIN web-app/images/apple-touch-icon-retina.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/apple-touch-icon.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/favicon.ico
Binary file not shown.
View
BIN web-app/images/grails_logo.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/grails_logo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/leftnav_btm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/leftnav_midstretch.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/leftnav_top.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/database_add.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/database_delete.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/database_edit.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/database_save.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/database_table.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/exclamation.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/house.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/information.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/shadow.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/sorted_asc.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/skin/sorted_desc.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/spinner.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN web-app/images/springsource.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
9 web-app/js/application.js
@@ -0,0 +1,9 @@
+if (typeof jQuery !== 'undefined') {
+ (function($) {
+ $('#spinner').ajaxStart(function() {
+ $(this).fadeIn();
+ }).ajaxStop(function() {
+ $(this).fadeOut();
+ });
+ })(jQuery);
+}

0 comments on commit 71f50e1

Please sign in to comment.