diff --git a/.gitignore b/.gitignore index beef00d0..9c4c9bcc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .project .settings target +lib_managed/ diff --git a/README.txt b/README.txt index b6009d08..5e7411c8 100644 --- a/README.txt +++ b/README.txt @@ -6,6 +6,8 @@ http://databinder.net/ Version History +1.2.1 Updates Databinder to the latest Wicket and Hibernate versions. + 1.2 This release represents a significant refactor of the Databinder toolkit to allow it to work with any underlying persistence technology. Core functionality now resides in modules like databinder-models, while specific functionality is in databinder-models-hib for Hibernate, or databinder-models-ao for ActiveObjects. The "databinder" dependency that was used through 1.1 is now databinder-app and databinder-app-hib for Hibernate applications. Migration to the new codebase is fairly straightforward. The first step is to refer to the appropriate new Maven artifact name, databinder-app-hib for most applications. Then update dependencies for your IDE, as there are number of class and page renames to work through. Some changes were made to accomodate the new persistence-agnostic structure, others are there just to bundle desired, breaking changes in one release. There aren't any major significant conceptual changes. The renames are as follows: diff --git a/databinder-app/src/main/java/net/databinder/auth/AuthDataSessionBase.java b/databinder-app/src/main/java/net/databinder/auth/AuthDataSessionBase.java index ae94b3d9..7b7adb61 100644 --- a/databinder-app/src/main/java/net/databinder/auth/AuthDataSessionBase.java +++ b/databinder-app/src/main/java/net/databinder/auth/AuthDataSessionBase.java @@ -110,7 +110,7 @@ public boolean signIn(String username, String password) { * @return true if signed in, false if credentials incorrect */ public boolean signIn(final String username, final String password, boolean setCookie) { - signOut(); + clearUser(); T potential = getUser(username); if (potential != null && (potential).getPassword().matches(password)) signIn(potential, setCookie); @@ -219,12 +219,18 @@ protected void detach() { userModel.detach(); } - /** Nullifies userModel and clears authentication cookies. */ - public void signOut() { + /** Nullifies userModela nd clears authentication cookies. */ + protected void clearUser() { userModel = null; CookieRequestCycle requestCycle = (CookieRequestCycle) RequestCycle.get(); requestCycle.clearCookie(getUserCookieName()); requestCycle.clearCookie(getAuthCookieName()); + } + + /** Signs out and invalidates session. */ + public void signOut() { + clearUser(); + getSessionStore().invalidate(RequestCycle.get().getRequest()); } } diff --git a/databinder-auth-components-ao/src/main/java/net/databinder/auth/components/ao/UserAdminPage.java b/databinder-auth-components-ao/src/main/java/net/databinder/auth/components/ao/UserAdminPage.java index f1a39782..facd219a 100644 --- a/databinder-auth-components-ao/src/main/java/net/databinder/auth/components/ao/UserAdminPage.java +++ b/databinder-auth-components-ao/src/main/java/net/databinder/auth/components/ao/UserAdminPage.java @@ -14,6 +14,7 @@ import org.apache.wicket.authorization.strategies.role.Roles; import org.apache.wicket.markup.html.form.Button; import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.model.IModel; /** * User administration page. Lists all users, allows editing usernames, passwords, and roles. @@ -56,15 +57,15 @@ protected void onSubmit() { @SuppressWarnings("unchecked") @Override protected void setPassword(String password) { - if (form.getEntityModel().isBound()) + if (getUserForm().getEntityModel().isBound()) super.setPassword(password); else - ((Map)form.getModelObject()).put("passwordHash", UserHelper.getHash(password)); + ((Map)getUserForm().getModelObject()).put("passwordHash", UserHelper.getHash(password)); } @Override protected Button deleteButton(String id) { - return form.new DeleteButton(id); + return getUserForm().new DeleteButton(id); } @Override diff --git a/databinder-auth-components-hib/src/main/java/net/databinder/auth/components/hib/UserAdminPage.java b/databinder-auth-components-hib/src/main/java/net/databinder/auth/components/hib/UserAdminPage.java index f2b93ff9..3fd1fbac 100644 --- a/databinder-auth-components-hib/src/main/java/net/databinder/auth/components/hib/UserAdminPage.java +++ b/databinder-auth-components-hib/src/main/java/net/databinder/auth/components/hib/UserAdminPage.java @@ -40,13 +40,13 @@ protected Button deleteButton(String id) { return new Button("delete") { @Override public void onSubmit() { - Databinder.getHibernateSession().delete(form.getModelObject()); + Databinder.getHibernateSession().delete(getUserForm().getModelObject()); Databinder.getHibernateSession().getTransaction().commit(); form.clearPersistentObject(); } @Override public boolean isEnabled() { - return !((AuthSession)getSession()).getUser().equals(form.getModelObject()) + return !((AuthSession)getSession()).getUser().equals(getUserForm().getModelObject()) && getBindingModel().isBound(); } }.setDefaultFormProcessing(false); diff --git a/databinder-auth-components/src/main/java/net/databinder/auth/components/UserAdminPageBase.java b/databinder-auth-components/src/main/java/net/databinder/auth/components/UserAdminPageBase.java index 9b65eaf1..af60736e 100644 --- a/databinder-auth-components/src/main/java/net/databinder/auth/components/UserAdminPageBase.java +++ b/databinder-auth-components/src/main/java/net/databinder/auth/components/UserAdminPageBase.java @@ -3,6 +3,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.zip.DataFormatException; import net.databinder.auth.AuthApplication; import net.databinder.auth.AuthSession; @@ -48,6 +49,10 @@ @AuthorizeInstantiation(Roles.ADMIN) public abstract class UserAdminPageBase extends WebPage { protected Form form; + protected Form getUserForm() { + return form; + } + public UserAdminPageBase() { add(new DataStyleLink("css")); add(new Label("title", new ResourceModel("data.auth.user_admin", "User Administration"))); diff --git a/databinder-dispatch-components/src/main/scala/net/databinder/dispatch/components/HttpPostConverter.scala b/databinder-dispatch-components/src/main/scala/net/databinder/dispatch/components/HttpPostConverter.scala index 3c3fc31e..a0973d27 100644 --- a/databinder-dispatch-components/src/main/scala/net/databinder/dispatch/components/HttpPostConverter.scala +++ b/databinder-dispatch-components/src/main/scala/net/databinder/dispatch/components/HttpPostConverter.scala @@ -15,11 +15,11 @@ import net.sf.ehcache.CacheManager import net.sf.ehcache.Ehcache import net.sf.ehcache.Element -import net.databinder.dispatch.HttpServer +import net.databinder.dispatch.Http abstract class HttpPostConverter extends AbstractConverter[String] { - def service = new HttpServer("localhost", 8180) + def service = new Http("localhost", 8180) def path_name: String def convertToObject(value: String, locale: Locale): String = null @@ -28,7 +28,7 @@ abstract class HttpPostConverter extends AbstractConverter[String] { override def convertToString(source: Object, locale: Locale) = try { HttpPostConverter.cache(path_name, source.hashCode()) { - (service("/" + path_name) << "input" -> source).as_str + (service("/" + path_name) << Map("input" -> source)).as_str } } catch { case e => diff --git a/databinder-dispatch/Rakefile b/databinder-dispatch/Rakefile deleted file mode 100644 index e23df595..00000000 --- a/databinder-dispatch/Rakefile +++ /dev/null @@ -1,9 +0,0 @@ -require "tasks/databinder.rb" - -repositories.remote << "http://repo1.maven.org/maven2/" - -define "databinder-dispatch" do - compile.with HTTPCLIENT - compile.options.deprecation = true - embed_server -end diff --git a/databinder-dispatch/project/build.properties b/databinder-dispatch/project/build.properties new file mode 100644 index 00000000..f6ba0837 --- /dev/null +++ b/databinder-dispatch/project/build.properties @@ -0,0 +1,4 @@ +#Project properties +#Sat Feb 21 09:31:49 EST 2009 +project.name=databinder-dispatch +project.version=1.2.2-SNAPSHOT diff --git a/databinder-dispatch/project/build/src/Dispatch.scala b/databinder-dispatch/project/build/src/Dispatch.scala new file mode 100644 index 00000000..15a53350 --- /dev/null +++ b/databinder-dispatch/project/build/src/Dispatch.scala @@ -0,0 +1,6 @@ +import sbt._ + +class Dispatch(info: ProjectInfo) extends DefaultProject(info) +{ + val httpclient = "org.apache.httpcomponents" % "httpclient" % "4.0-beta2" +} diff --git a/databinder-dispatch/scratch.scala b/databinder-dispatch/scratch.scala index de6da671..e71f38fd 100644 --- a/databinder-dispatch/scratch.scala +++ b/databinder-dispatch/scratch.scala @@ -48,7 +48,5 @@ implicit def any2anyExtras(x: Any) = new AnyExtras(x) import net.databinder.dispatch._ -val svc = Http.host("services.newsgator.com").on_x(_.addHeader("X-NGAPIToken", token)).auth(user, pass) -svc("/ngws/svc/Location.aspx") >>> System.out - - +val friday = new Database("friday") +friday.all_docs diff --git a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Couch.scala b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Couch.scala index 72b40283..a3e8f3e7 100644 --- a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Couch.scala +++ b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Couch.scala @@ -1,6 +1,8 @@ -package net.databinder.dispatch +package net.databinder.dispatch.couch import java.io.InputStream +import java.net.URLEncoder.encode +import org.apache.http.HttpHost trait Doc extends Schema { val _id = String(Symbol("_id")) @@ -9,6 +11,33 @@ trait Doc extends Schema { object Doc extends Doc +object Couch { + def apply(host: String) = new Http(host, 5984) + def apply(): Http = Couch("127.0.0.1") +} + + +case class Database(name: String) { + class H(val http: Http) extends Database(name) { + def apply(id: String): Http#Request = http("/" + name + "/" + encode(id)) + def all_docs = { + val Some(rows) = (this("_all_docs") >> { new Store(_) })(Listing.rows) + for { + Some(row) <- rows + id <- (new Store(row))(ListItem.id) + } yield id + } + } + def apply(http: Http) = new H(http) + + object ListItem extends Schema { + val id = String('id) + } + object Listing extends Schema { + val rows = List[Map[Symbol, Option[Any]]]('rows) + } +} + object Revise extends Schema { val id = String('id) val rev = String('rev) diff --git a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Http.scala b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Http.scala index b3714e91..2ff23daf 100644 --- a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Http.scala +++ b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Http.scala @@ -7,6 +7,7 @@ import org.apache.http.client._ import org.apache.http.impl.client.DefaultHttpClient import org.apache.http.client.methods._ import org.apache.http.client.entity.UrlEncodedFormEntity +import org.apache.http.client.utils.URLEncodedUtils import org.apache.http.entity.StringEntity import org.apache.http.message.BasicNameValuePair @@ -15,13 +16,37 @@ import org.apache.http.params.{HttpProtocolParams, BasicHttpParams} import org.apache.http.util.EntityUtils import org.apache.http.auth.{AuthScope, UsernamePasswordCredentials} -case class UnexpectedResponse(code: Int) extends Exception("Unexpected resoponse code: " + code) +case class StatusCode(code: Int, contents:String) + extends Exception("Exceptional resoponse code: " + code + "\n" + contents) -trait Http { - val client: HttpClient - - /** Execute in HttpClient. */ - protected def execute(req: HttpUriRequest) = client.execute(req) +class Http( + val host: Option[HttpHost], + val headers: List[(String, String)], + val creds: Option[(String, String)] +) { + def this(host: HttpHost) = this(Some(host), Nil, None) + def this(hostname: String, port: Int) = this(new HttpHost(hostname, port)) + def this(hostname: String) = this(new HttpHost(hostname)) + lazy val client = new ConfiguredHttpClient { + for (h <- host; (name, password) <- creds) { + getCredentialsProvider.setCredentials( + new AuthScope(h.getHostName, h.getPort), + new UsernamePasswordCredentials(name, password) + ) + } + } + + /** Uses bound host server in HTTPClient execute. */ + def execute(req: HttpUriRequest):HttpResponse = { + host match { + case None => client.execute(req) + case Some(host) => client.execute(host, req) + } + } + /** Sets authentication credentials for bound host. */ + def as (name: String, pass: String) = new Http(host, headers, Some((name, pass))) + /** Add header */ + def << (k: String, v: String) = new Http(host, (k,v) :: headers, creds) /** Get wrapper */ def g [T](uri: String) = x[T](new HttpGet(uri)) @@ -29,20 +54,22 @@ trait Http { /** eXecute wrapper */ def x [T](req: HttpUriRequest) = new { /** handle response codes, response, and entity in thunk */ - def apply(thunk: (Int, HttpResponse, HttpEntity) => T) = { + def apply(thunk: (Int, HttpResponse, Option[HttpEntity]) => T) = { val res = execute(req) - res.getEntity match { - case null => error("no response message") - case ent => try { - thunk(res.getStatusLine.getStatusCode, res, ent) - } finally { ent.consumeContent() } + val ent = res.getEntity match { + case null => None + case ent => Some(ent) } + try { thunk(res.getStatusLine.getStatusCode, res, ent) } + finally { ent foreach (_.consumeContent) } } /** Handle reponse entity in thunk if reponse code returns true from chk. */ - def when(chk: Int => Boolean)(thunk: HttpEntity => T) = this { (code, res, ent) => - if (chk(code)) thunk(ent) - else throw UnexpectedResponse(code) + def when(chk: Int => Boolean)(thunk: (HttpResponse, Option[HttpEntity]) => T) = this { (code, res, ent) => + if (chk(code)) thunk(res, ent) + else throw StatusCode(code, + ent.map(EntityUtils.toString(_, HTTP.UTF_8)).getOrElse("") + ) } /** Handle reponse entity in thunk when response code is 200 - 204 */ @@ -54,38 +81,46 @@ trait Http { /** Wrapper to handle common requests, preconfigured as response wrapper for a * get request but defs return other method responders. */ - class Request(uri: String) extends Respond(new HttpGet(uri)) { + class Request(req: HttpUriRequest) { + headers foreach { case (k, v) => req.addHeader(k, v) } + def this(uri: String) = this(new HttpGet(uri)) /** Put the given object.toString and return response wrapper. */ def <<< (body: Any) = { - val m = new HttpPut(uri) + val m = new HttpPut(req.getURI) m setEntity new StringEntity(body.toString, HTTP.UTF_8) HttpProtocolParams.setUseExpectContinue(m.getParams, false) - new Respond(m) + new Request(m) } + /** Convert repeating name value tuples to list of pairs for httpclient */ + private def map2ee(values: Map[String, Any]) = + new java.util.ArrayList[BasicNameValuePair](values.size) { + values.foreach { case (k, v) => add(new BasicNameValuePair(k, v.toString)) } + } /** Post the given key value sequence and return response wrapper. */ - def << (values: (String, Any)*) = { - val m = new HttpPost(uri) - m setEntity new UrlEncodedFormEntity( - java.util.Arrays.asList( - (values map { case (k, v) => new BasicNameValuePair(k, v.toString) }: _*) - ), - HTTP.UTF_8 - ) - new Respond(m) + def << (values: Map[String, Any]) = { + val m = new HttpPost(req.getURI) + m setEntity new UrlEncodedFormEntity(map2ee(values), HTTP.UTF_8) + new Request(m) + } + /** Get with query parameters */ + def ?< (values: Map[String, Any]) = + new Request(new HttpGet(req.getURI + "?" + URLEncodedUtils.format( + map2ee(values), HTTP.UTF_8 + ))) + def apply [T] (thunk: (Int, HttpResponse, Option[HttpEntity]) => T) = x (req) (thunk) + /** Handle response and entity in thunk if OK. */ + def ok [T] (thunk: (HttpResponse, Option[HttpEntity]) => T) = x (req) ok (thunk) + /** Handle response entity in thunk if OK. */ + def okee [T] (thunk: HttpEntity => T): T = ok { + case (_, Some(ent)) => thunk(ent) + case (res, _) => error("response has no entity: " + res) } - /** Post the given map and return response wrapper. */ - def << (values: Map[String, Any]): Respond = <<(values.toList: _*) - } - /** Wrapper for common response handling. */ - class Respond(req: HttpUriRequest) { /** Handle InputStream in thunk if OK. */ - def >> [T] (thunk: InputStream => T) = x (req) ok (res => thunk(res.getContent)) - /** Ignore response body if OK. */ - def >| = x (req) ok (res => ()) + def >> [T] (thunk: InputStream => T) = okee (ent => thunk(ent.getContent)) /** Return response in String if OK. (Don't blow your heap, kids.) */ - def as_str = x (req) ok { EntityUtils.toString(_, HTTP.UTF_8) } + def as_str = okee { EntityUtils.toString(_, HTTP.UTF_8) } /** Write to the given OutputStream. */ - def >>> (out: OutputStream): Unit = x (req) ok { _.writeTo(out) } + def >>> (out: OutputStream): Unit = okee { _.writeTo(out) } /** Process response as XML document in thunk */ def <> [T] (thunk: (scala.xml.NodeSeq => T)) = { // an InputStream source is the right way, but ConstructingParser @@ -95,11 +130,12 @@ trait Http { val src = scala.io.Source.fromString(in) thunk(scala.xml.parsing.XhtmlParser(src)) } + /** Ignore response body if OK. */ + def >| = ok ((r,e) => ()) } } -/** DefaultHttpClient with parameters that may be more widely compatible. */ -class ConfiguredHttpClient extends DefaultHttpClient { +class ConfiguredHttpClient extends DefaultHttpClient { override def createHttpParams = { val params = new BasicHttpParams HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1) @@ -109,42 +145,13 @@ class ConfiguredHttpClient extends DefaultHttpClient { } } -/** client value initialized to a CovfiguredHttpClient instance. */ -trait ConfiguredHttp extends Http { - lazy val client = new ConfiguredHttpClient -} - -/** For interaction with a single HTTP host. */ -class HttpServer(host: HttpHost) extends ConfiguredHttp { - def this(hostname: String, port: Int) = this(new HttpHost(hostname, port)) - def this(hostname: String) = this(new HttpHost(hostname)) - /** Uses bound host server in HTTPClient execute. */ - override def execute(req: HttpUriRequest):HttpResponse = { - preflight(req) - client.execute(host, req) - } - /** Block to be run before every outgoing request */ - private var preflight = { req: HttpUriRequest => () } - /** @param action run before every outgoing request */ - protected def preflight(action: HttpUriRequest => Unit) { - preflight = action - } - /** Sets authentication credentials for bound host. */ - protected def auth(name: String, password: String) { - client.getCredentialsProvider.setCredentials( - new AuthScope(host.getHostName, host.getPort), - new UsernamePasswordCredentials(name, password) - ) - } -} - import org.apache.http.conn.scheme.{Scheme,SchemeRegistry,PlainSocketFactory} import org.apache.http.conn.ssl.SSLSocketFactory import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager /** May be used directly from any thread, or to return configured single-thread instances. */ -object Http extends Http { - lazy val client = new ConfiguredHttpClient { +object Http extends Http(None, Nil, None) { + override lazy val client = new ConfiguredHttpClient { override def createClientConnectionManager() = { val registry = new SchemeRegistry() registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)) diff --git a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Schema.scala b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Schema.scala index 75bd2b6e..7d8abbe8 100644 --- a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Schema.scala +++ b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Schema.scala @@ -18,12 +18,11 @@ trait Schema { } case class Spec[T](symbol: Symbol, to: Any => T, from: T => Any) extends Value[T](symbol) { - import org.apache.http.impl.cookie.DateUtils override def to_type(opt: Option[Any]) = opt map to override def from_type(opt: Option[T]) = opt map from } - case class List[T](symbol: Symbol) extends Value[scala.List[T]](symbol) + case class List[T](symbol: Symbol) extends Value[scala.List[Option[T]]](symbol) case class Object(symbol: Symbol) extends Value[Map[Symbol, Option[Any]]](symbol) with Schema { override def loc(base: Option[Map[Symbol, Option[Any]]], sub_sym: Symbol) = diff --git a/databinder-dispatch/src/main/scala/net/databinder/dispatch/Times.scala b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Times.scala new file mode 100644 index 00000000..3af6e096 --- /dev/null +++ b/databinder-dispatch/src/main/scala/net/databinder/dispatch/Times.scala @@ -0,0 +1,37 @@ +package net.databinder.dispatch.times + +trait Times { + lazy val http = new Http("api.nytimes.com") + val api_key: String + val service: String + val version: Int + + def exec(action: String, params: Map[String, Any]) = + http( + ("/svc" :: service :: "v" + version :: action :: Nil).mkString("/") + ) ?< (params + ("api-key" -> api_key)) + + def exec(action: String): Http#Request = exec(action, Map[String, Any]()) +} + +case class People(api_key: String) extends Times { + val service = "timespeople/api"; + val version = 1 + + def profile(user_id: Int) = exec("/user/" + user_id + "/profile.js"); +} + +case class Search(api_key: String) extends Times { + val service = "search" + val version = 1 + + def search(query: String) = exec("/article", Map("query" -> query)) +} + +case class Community(api_key: String) extends Times { + val service = "community" + val version = 2 + + def recent = exec("comments/recent.json") +} + diff --git a/databinder-dispatch/tasks b/databinder-dispatch/tasks deleted file mode 160000 index b491e32c..00000000 --- a/databinder-dispatch/tasks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b491e32c9271fd55e4954bfb38c68fae708203c4 diff --git a/databinder-models-hib/src/main/java/net/databinder/models/hib/HibernateProvider.java b/databinder-models-hib/src/main/java/net/databinder/models/hib/HibernateProvider.java index 417662f7..f9b673de 100644 --- a/databinder-models-hib/src/main/java/net/databinder/models/hib/HibernateProvider.java +++ b/databinder-models-hib/src/main/java/net/databinder/models/hib/HibernateProvider.java @@ -113,6 +113,7 @@ public HibernateProvider(final String query, final String countQuery) { * Provides entities matching the given query with bound parameters. The count query * is derived by prefixing "select count(*)" to the given query; this will fail if * the supplied query has a select clause. + * @deprecated because the derived count query is often non-standard, even if it works. Use the longer constructor. */ public HibernateProvider(String query, QueryBinder queryBinder) { this(query, queryBinder, makeCount(query), queryBinder); @@ -134,7 +135,10 @@ public HibernateProvider(QueryBuilder queryBuilder, QueryBuilder countQueryBuild this.countQueryBuilder = countQueryBuilder; } - /** @return query with select count(*) prepended */ + /** + * @deprecated + * @return query with select count(*) prepended + */ static protected String makeCount(String query) { return "select count(*) " + query; } diff --git a/pom.slip b/pom.slip index d412d2d2..a82733e7 100644 --- a/pom.slip +++ b/pom.slip @@ -18,7 +18,7 @@ project(xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance",xmlns="http://mave properties: wicket.version: "1.4-rc1" - scala.version: "2.7.2" + scala.version: "2.7.5" packaging: "pom" modules: diff --git a/pom.xml b/pom.xml index 5266f77b..fc4290b7 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ 1.4-rc1 - 2.7.2 + 2.7.5 pom