diff --git a/src/main/scala/app/CreateRepositoryServlet.scala b/src/main/scala/app/CreateRepositoryServlet.scala index f620ab3..0a3665c 100644 --- a/src/main/scala/app/CreateRepositoryServlet.scala +++ b/src/main/scala/app/CreateRepositoryServlet.scala @@ -1,6 +1,7 @@ package app import util.Directory._ +import util.Validations._ import org.scalatra._ import java.io.File import org.eclipse.jgit.api.Git @@ -23,11 +24,8 @@ * Create new repository. */ post("/") { - withValidation(validate, params){ - val repositoryName = params("name") - val description = params("description") - - val gitdir = getRepositoryDir(LoginUser, repositoryName) + withValidation(form, params){ form => + val gitdir = getRepositoryDir(LoginUser, form.name) val repository = new RepositoryBuilder().setGitDir(gitdir).setBare.build repository.create @@ -36,16 +34,16 @@ config.setBoolean("http", null, "receivepack", true) config.save - val tmpdir = getInitRepositoryDir(LoginUser, repositoryName) + val tmpdir = getInitRepositoryDir(LoginUser, form.name) try { // Clone the repository Git.cloneRepository.setURI(gitdir.toURI.toString).setDirectory(tmpdir).call // Create README.md - FileUtils.writeStringToFile(new File(tmpdir, "README.md"), if(description.nonEmpty){ - repositoryName + "\n===============\n\n" + description + FileUtils.writeStringToFile(new File(tmpdir, "README.md"), if(form.description.nonEmpty){ + form.name + "\n===============\n\n" + form.description } else { - repositoryName + "\n===============\n" + form.name + "\n===============\n" }, "UTF-8") val git = Git.open(tmpdir) @@ -58,25 +56,32 @@ } // redirect to the repository - redirect("/%s/%s".format(LoginUser, repositoryName)) + redirect("/%s/%s".format(LoginUser, form.name)) } } get("/validate") { contentType = "application/json" - validate(params).toJSON + form.validateAsJSON(params) } - def validate(params: Map[String, String]): ValidationResult = { - val name = params("name") - if(name.isEmpty){ - ValidationResult(false, Map("name" -> "Repository name is required.")) - } else if(!name.matches("^[a-z0-6\\-_]+$")){ - ValidationResult(false, Map("name" -> "Repository name contans invalid character.")) - } else if(getRepositories(LoginUser).contains(name)){ - ValidationResult(false, Map("name" -> "Repository already exists.")) - } else { - ValidationResult(true, Map.empty) + val form = Form( + "name" -> trim(label("Repository name", text(required, maxlength(40), repository))), + "description" -> trim(label("Description" , text())) + )(RepositoryCreationForm.apply) + + def repository: Constraint = new Constraint(){ + def validate(name: String, value: String): Option[String] = { + if(!value.matches("^[a-z0-9\\-_]+$")){ + Some("Repository name contains invalid character.") + } else if(getRepositories(LoginUser).contains(value)){ + Some("Repository already exists.") + } else { + None + } } } + + case class RepositoryCreationForm(name: String, description: String) + } \ No newline at end of file diff --git a/src/main/scala/app/ServletBase.scala b/src/main/scala/app/ServletBase.scala index 8f5cd04..2f6af60 100644 --- a/src/main/scala/app/ServletBase.scala +++ b/src/main/scala/app/ServletBase.scala @@ -15,22 +15,4 @@ // TODO get from session val LoginUser = System.getProperty("user.name") - protected def withValidation(validator: Map[String, String] => ValidationResult, params: Map[String, String])(action: => Any): Any = { - validator(params).valid match { - case true => action - case false => throw new RuntimeException("Invalid Request") // TODO show error page? - } - } - - case class ValidationResult(valid: Boolean, errors: Map[String, String]){ - def toJSON(): JObject = { - JObject( - "valid" -> JBool(valid), - "errors" -> JObject(errors.map { case (key, value) => - JField(key, JString(value)) - }.toList) - ) - } - } - } \ No newline at end of file diff --git a/src/main/scala/util/Validations.scala b/src/main/scala/util/Validations.scala new file mode 100644 index 0000000..0019214 --- /dev/null +++ b/src/main/scala/util/Validations.scala @@ -0,0 +1,118 @@ +package util + +import org.json4s._ +import org.json4s.jackson._ + +object Validations { + + def withValidation[T](form: Form[T], params: Map[String, String])(action: T => Any): Any = { + form.validate(params).isEmpty match { + case true => action(form.create(params)) + case false => throw new RuntimeException("Invalid Request") // TODO show error page? + } + } + + def Form[T, P1](f1: (String, ValueType[P1]))(factory: (P1) => T): Form[T] = new Form[T]{ + def fields = Seq(f1) + def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1))) + } + + def Form[T, P1, P2](f1: (String, ValueType[P1]), f2: (String, ValueType[P2]))(factory: (P1, P2) => T): Form[T] = new Form[T]{ + def fields = Seq(f1, f2) + def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1)), f2._2.convert(params(f2._1))) + } + + def Form[T, P1, P2, P3](f1: (String, ValueType[P1]), f2: (String, ValueType[P2]), f3: (String, ValueType[P3]))(factory: (P1, P2, P3) => T): Form[T] = new Form[T]{ + def fields = Seq(f1, f2) + def create(params: Map[String, String]) = factory(f1._2.convert(params(f1._1)), f2._2.convert(params(f2._1)), f3._2.convert(params(f3._1))) + } + + abstract class Form[T] { + + def fields: Seq[(String, ValueType[_])] + + def create(params: Map[String, String]): T + + def validate(params: Map[String, String]): Map[String, String] = { + fields.map { case (name, valueType) => + valueType.validate(name, params(name)) match { + case Some(message) => Some(name, message) + case None => None + } + }.flatten.toMap + } + + def validateAsJSON(params: Map[String, String]): JObject = { + JObject(validate(params).map { case (key, value) => + JField(key, JString(value)) + }.toList) + } + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // Constraints + + trait Constraint { + def validate(name: String, value: String): Option[String] + } + + def required: Constraint = new Constraint(){ + def validate(name: String, value: String): Option[String] = + if(value.isEmpty) Some("%s is required.".format(name)) else None + } + + def required(message: String): Constraint = new Constraint(){ + def validate(name: String, value: String): Option[String] = + if(value.isEmpty) Some(message) else None + } + + def maxlength(length: Int): Constraint = new Constraint(){ + def validate(name: String, value: String): Option[String] = + if(value.length > length) Some("%s cannot be longer than %d characters.".format(name, length)) else None + } + + def minlength(length: Int): Constraint = new Constraint(){ + def validate(name: String, value: String): Option[String] = + if(value.length < length) Some("%s cannot be shorter than %d characters".format(name, length)) else None + } + + def pattern(pattern: String, message: String = ""): Constraint = new Constraint { + def validate(name: String, value: String): Option[String] = + if(!value.matches("^" + pattern + "$")){ + if(message.isEmpty) Some("%s must be '%s'.".format(name, pattern)) else Some(message) + } else None + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // ValueTypes + + abstract class ValueType[T](constraints: Constraint*) { + + def convert(value: String): T + + def validate(name: String, value: String): Option[String] = { + constraints.map(_.validate(name, value)).flatten.headOption + } + } + + def text(constraints: Constraint*): ValueType[String] = new ValueType[String](constraints: _*){ + def convert(value: String): String = value + } + + ///////////////////////////////////////////////////////////////////////////////////////////// + // ValueType wrappers to provide additional features. + + def trim[T](valueType: ValueType[T]): ValueType[T] = new ValueType[T](){ + def convert(value: String): T = valueType.convert(value.trim) + override def validate(name: String, value: String): Option[String] = valueType.validate(name, value.trim) + } + + /** + * + */ + def label[T](label: String, valueType: ValueType[T]): ValueType[T] = new ValueType[T](){ + def convert(value: String): T = valueType.convert(value.trim) + override def validate(name: String, value: String): Option[String] = valueType.validate(label, value.trim) + } + +} \ No newline at end of file diff --git a/src/main/webapp/assets/common/js/validation.js b/src/main/webapp/assets/common/js/validation.js index 330f22c..1e37e6e 100644 --- a/src/main/webapp/assets/common/js/validation.js +++ b/src/main/webapp/assets/common/js/validation.js @@ -16,11 +16,11 @@ // clear all error messages $('.error-message').text(''); - if(data.valid){ + if($.isEmptyObject(data)){ form.data('validated', true); form.submit(); } else { - $.each(data.errors, function(key, value){ + $.each(data, function(key, value){ $('#error-' + key).text(value); }); }