Don't use those libraries - instead use the good old grails-xfire plugin. This guy just works out of the box with .NET 3.5 and 4.0. In C# just add a web reference and you're all set!
More on this topic soon - stay tuned!
web.config
getting this guy to work was just a matter of minutes. dataSource.logSql
configurable from an external file so that after deployment I can decide if I want to see the SQL statements in logs or not.dataSource.url
parameter in a properties file designated as an external configuration file I did exactly the same with the logSql
option. It turned out not to work.dataSource
as follows:dataSource {
logSql = false // disable SQL statements logging
}
environments
block like this:// set per-environment serverURL stem for creating absolute links
environments {
production {
grails.config.locations = [ "file:/etc/example/example-config.groovy" ]
grails.serverURL = "http://www.changeme.com"
}
...
dataSource
that previously used an embedded HSQL database to use MySQL:dataSource {
logSql = false
driverClassName = 'com.mysql.jdbc.Driver'
dialect = 'org.hibernate.dialect.MySQL5InnoDBDialect'
dbCreate = 'update'
url = 'jdbc:mysql://127.0.0.1:3306/example'
username = 'root'
password = ....
}
Obviously since there's no MySQL JDBC driver in my application I've had to supply one to the common libraries folder in Tomcat (in Ubuntu if you install Tomcat via apt-get then the folder is /var/lib/tomcat6/common
, otherwise it's just the lib
folder in your tomcat installation) and all worked just perfectly :)log4j = {
appenders {
file name: "application", file: "/var/example/log/example.log"
file name: "stacktrace", file: "/var/example/log/stacktrace.log"
}
debug application: 'grails.app'
(the rest of usual logging settings here...)
}
.application.gsp
or main.gsp
seem like a perfect match for application-wide layouts. The problem with main.gsp
is that it's already taken by the scaffolding mechanism and it's very unlikely that the whole application will be based on scaffolded views. The problem with application.gsp
however is much more severe. Once you create this guy it'll apply to everything even if there's no layout specified. This also applies not only to views but to static resources as well! So before you go ahead and create the application.gsp
file be prepared to what's coming on to you.conf/Config.groovy
file:grails.sitemesh.default.layout = "none"
none.gsp
will be used as the catch-all instead of application.gsp
and so I can use the latter one for my evil purposes :)application.gsp
looks just wrong all along.
script
folder that contained scripts to execute. server
, generate
those are only a few of the examples. In Grails it is different: you get a global command called grails
that invokes scripts from either the scripts
folder within your project or from a global folder. Well, the new RoR works exactly the same way :) There's a rails
command that does exactly the same work :)appliaction.properties
file listing all the required plugins in their respective versions. In RoR 3.0 there's a Gemfile
file serving exactly the same purpose :)class ScriptsController {
def home = { }
}
In this case there are no variables passed on from controller to the view but as you can imagine there are possibilities to do that.<script
type="text/javascript"
src="${g.createLink(controller: 'script', action: 'home')}.js">
</script>
and instead of rendering plain HTML code from my GSP I'm rendering JavaScript from /views/scripts/home.js.gsp:<%@ page contentType="text/javascript"%>
var HomeLink = "${g.createLink(controller: 'home', action: 'index')}";
class ExampleTagLib {
static namespace = 'g'
def script = { attrs ->
def link = g.createLink(
controller: 'scripts',
action: attrs.name ?: controllerName)
out << """<script type="text/javascript" src="${link}.js"></script>"""
}
}
So here I ask for the current controller's name (which is btw nicely preprocessed by Grails) and use that instead of passing in the same parameter value every time I need it even if the dynamic portion is named the same as the controller that called it. A convention, so to speak.<g:script />
What this will do it will look-up the current controller name and render a script
tag that will access the outcome of an action with the same name as the current controller as the src
. This is exactly the same as the first version if called from HomeController
only shorter.name
parameter comes in handy<g:script name="common" />
This way the common
script will be requested.java.io.IOException: Server returned HTTP response code: 407 for URL: http://cobertura.sourceforge.net/xml/coverage-04.dtd
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.setupCurrentEntity(XMLEntityManager.java:677)
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startEntity(XMLEntityManager.java:1315)
at com.sun.org.apache.xerces.internal.impl.XMLEntityManager.startDTDEntity(XMLEntityManager.java:1282)
at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.setInputSource(XMLDTDScannerImpl.java:283)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.dispatch(XMLDocumentScannerImpl.java:1194)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDriver.next(XMLDocumentScannerImpl.java:1090)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$PrologDriver.next(XMLDocumentScannerImpl.java:1003)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(XMLDocumentScannerImpl.java:648)
at com.sun.org.apache.xerces.internal.impl.XMLNSDocumentScannerImpl.next(XMLNSDocumentScannerImpl.java:140)
at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImpl.scanDocument(XMLDocumentFragmentScannerImpl.java:511)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:808)
at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(XML11Configuration.java:737)
at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(XMLParser.java:119)
at com.sun.org.apache.xerces.internal.parsers.AbstractSAXParser.parse(AbstractSAXParser.java:1205)
at com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser.parse(SAXParserImpl.java:522)
at _Events.replaceClosureNamesInXmlReports(_Events.groovy:118)
at _Events$replaceClosureNamesInXmlReports.callCurrent(Unknown Source)
at _Events.replaceClosureNamesInReports(_Events.groovy:89)
at _Events$_run_closure2.doCall(_Events.groovy:42)
at _GrailsEvents_groovy$_run_closure5.doCall(_GrailsEvents_groovy:58)
at _GrailsEvents_groovy$_run_closure5.call(_GrailsEvents_groovy)
at _GrailsTest_groovy$_run_closure1.doCall(_GrailsTest_groovy:203)
at TestApp$_run_closure1.doCall(TestApp.groovy:82)
at gant.Gant$_dispatch_closure5.doCall(Gant.groovy:381)
at gant.Gant$_dispatch_closure7.doCall(Gant.groovy:415)
at gant.Gant$_dispatch_closure7.doCall(Gant.groovy)
at gant.Gant.withBuildListeners(Gant.groovy:427)
at gant.Gant.this$2$withBuildListeners(Gant.groovy)
at gant.Gant$this$2$withBuildListeners.callCurrent(Unknown Source)
at gant.Gant.dispatch(Gant.groovy:415)
at gant.Gant.this$2$dispatch(Gant.groovy)
at gant.Gant.invokeMethod(Gant.groovy)
at gant.Gant.executeTargets(Gant.groovy:590)
at gant.Gant.executeTargets(Gant.groovy:589)
render
method. All would be nice and dandy if it'd be the controller itself but it turns out the command object is more like a domain class (with its ability to verify field values and so on) than anything else. But... there's hope!private getController() {
RequestContextHolder.
requestAttributes.
request.
getAttribute(GrailsApplicationAttributes.CONTROLLER)
}
controller
that we can use to call the render
method. controller.session
controller.request
controller.response
params
element of the controller is available!if(System.properties["${appName}.config.location"]) {
grails.config.locations << "file:" + System.properties["${appName}.config.location"]
}
log4j:ERROR Error initializing log4j: No signature of method:
groovy.util.ConfigObject.leftShift() is applicable for argument
types: (java.lang.String) values: [file:]
Possible solutions: leftShift(java.util.Map$Entry), leftShift(java.util.Map)
groovy.lang.MissingMethodException: No signature of method:
groovy.util.ConfigObject.leftShift() is applicable for argument types:
(java.lang.String) values: [file:]
Possible solutions: leftShift(java.util.Map$Entry), leftShift(java.util.Map)
at Config.run(Config.groovy:17)
2010-12-18 22:30:46 org.apache.catalina.core.StandardContext start
SEVERE: Error listenerStart
2010-12-18 22:30:46 org.apache.catalina.core.StandardContext start
SEVERE: Context [/] startup failed due to previous errors
grails.config.locations
element is not set by default. To fix this simply add the following line just before the the code you've just uncommented:
grails.config.locations = []
if(System.getenv()["${appName}.config.location"]) {
grails.config.locations << "file:" + System.getenv()["${appName}.config.location"]
}
render
method and specify the template to be rendered like in this example:class ExampleTagLib {
def example = { attr, body ->
out << render(template: '/tags/example')
}
}
render
method like this:class ExampleTagLib {
def example = { attr, body ->
out << render(template: '/tags/example', plugin: 'example-plugin')
}
}
package com.aplaline.example.test;
import org.codehaus.groovy.grails.test.GrailsTestTargetPattern;
import org.codehaus.groovy.grails.test.junit4.runner.GrailsTestCaseRunner;
@SuppressWarnings("rawtypes")
public class GrailsJUnit4Runner extends GrailsTestCaseRunner {
public GrailsJUnit4Runner(Class testClass) {
super(testClass, new GrailsTestTargetPattern[0]);
}
}
package com.aplaline.example
import grails.test.*
import org.junit.Test;
import org.junit.runner.RunWith;
import com.aplaline.example.test.GrailsJUnit4Runner;
@RunWith(GrailsJUnit4Runner)
class CalculatorServiceTests extends GrailsUnitTestCase {
@Test
void canAddTwoNumbers() {
// given
def service = new CalculatorService()
// when
def actual = service.sum(1, 2)
// then
assert actual == 3
}
}
type
IHello = interface
['{8576CE04-E24A-11D4-BDE0-00A024BAF736}']
procedure DisplayMessage1(S: PChar); safecall;
procedure DisplayMessage2(S: WideString); safecall;
end;
// I've not found where the "ShowMessage" is declared...
procedure ShowMessage(Msg: AnsiString);
begin
MessageBox(0, PChar(Msg), 'Information', MB_OK or MB_ICONINFORMATION);
end;
{ THelloImpl }
procedure THelloImpl.DisplayMessage1(Msg: PChar); safecall;
begin
ShowMessage('MESSAGE USING PCHAR [' + Msg + ']');
end;
procedure THelloImpl.DisplayMessage2(Msg: WideString); safecall;
begin
ShowMessage('MESSAGE USING WIDESTRING [' + Msg + ']');
end;
const
CLASS_Hello: TGUID = '{8576CE02-E24A-11D4-BDE0-00A024BAF736}';
TComObjectFactory.Create(
ComServer,
THelloImpl,
CLASS_Hello,
'Hello',
'An example COM Object!',
ciMultiInstance,
tmApartment);
CoHello = class
class function Create: IHello;
class function CreateRemote(const MachineName: String): IHello;
end;
[...]
{ CoHello }
class function CoHello.Create: IHello;
begin
Result := CreateComObject(CLASS_Hello) as IHello;
end;
class function CoHello.CreateRemote(const MachineName: String): IHello;
begin
Result := CreateRemoteComObject(MachineName, CLASS_Hello) as IHello;
end;
var
Hello: IHello;
begin
CoInitialize(0);
Hello := CoHello.Create;
Hello.DisplayMessage1('Hello, world! using COM from FreePascal or Delphi');
Hello.DisplayMessage2('Hello, world! using COM from FreePascal or Delphi');
end.
using System;
using System.Runtime.InteropServices;
namespace Client.Net {
[Guid("8576CE04-E24A-11D4-BDE0-00A024BAF736")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IHello {
void DisplayMessage1(
[In, MarshalAs(UnmanagedType.LPStr)] string message
);
void DisplayMessage2(
[In, MarshalAs(UnmanagedType.BStr)] string message
);
}
[ComImport]
[Guid("8576CE02-E24A-11D4-BDE0-00A024BAF736")]
class Hello { }
class Program {
[STAThread]
public static void Main(String[] args) {
var hello = new Hello() as IHello;
hello.DisplayMessage1("Hello, world! using COM from C#");
hello.DisplayMessage2("Hello, world! using COM from C#");
}
}
}
static mapping = {
id composite: [ 'reportId', 'managerId' ]
}
class CarController {
def index = { ... }
def setCarAvailability = {
def car = Car.get(params.id)
if (car && params.available) {
car.avaiable = params.available
car.save(flush: true)
redirect action: 'index'
} else {
flash.message = "Error: either no valid car id, available not given or saving failed"
redirect action: 'index'
}
}
}
class CarController {
def index = { ... }
def setCarAvailability = {
SetCarAvailabilityCommand command ->
if (command.validate()) {
command.execute()
redirect action: 'index'
} else {
flash.message =
command.errors.allErrors.collect { error ->
error -> error.field
}.join(", ")
redirect action: 'index'
}
}
}
class SetCarAvailabilityCommand {
Car car
Boolean available
static constraints = {
car nullable: false, validator: { it?.attached }
available blank: false, required: false
}
def execute() {
car.available = available
car.save(flush: true)
}
}
How about that, ha? Nice separation of concerns (actual entity that does the job separated from controller that should only care about controlling what should go next), automatic parameter validation, clean naming and if you put those two into the same file then even automatic reloading works! <g:form action="setCarAvailability">
<g:textField name="car.id"/>
<g:checkBox name="available"/>
<br/><br/>
<g:submitButton name="action" />
</g:form>
grails create-app exampleStep 2: Install the grails-easyb plugin.
grails install-plugin easybStep 3: Install selenium rc.
java -jar selenium-server.jarStep 4: Create the home controller.
grails create-controller homeStep 5: Add some meat to the index action.
[ message: "Hello, world!" ]Step 6: Create a functional (yes! functional test at last!) in test/functional (this folder does not exist at first so you have to create it yourself). I'll call it MyFirstFunctional.story
import com.thoughtworks.selenium.*
before "start selenium", {
given "selenium is up and running", {
selenium = new DefaultSelenium(
"localhost", 4444, "*iexplore",
"http://localhost:8080/example/"
)
selenium.start()
}
}
scenario "Will show 'hello, world!' message", {
when "home/index opens", {
selenium.open("home/index")
}
then "the text 'Hello, world! appears on the page", {
selenium.isTextPresent("Hello, world!")
}
}
after "stop selenium", {
then "selenium should be shutdown", {
selenium.stop()
}
}
WARNING: you might naturally want to move the "after" section right below the "before" section. DON'T EVER DO THAT! EasyB test segments are executed in the order they have been declared so if you do that selenium will get stopped before any scenario can be executed!grails test-app functional:easybStep 8: Create the view (/views/home/index.gsp).
Message: ${message}Step 9: Execute the test and see it pass:
grails test-app functional:easyb
class HomeController {
def index = { }
def save = { LoginCommand login ->
println "login.username: ${login.username}"
println "login.password: ${login.password}"
println "comment.text: ${comment.text}"
redirect action: "index"
}
}
class LoginCommand {
String username
String password
}
<g:form action="save">
<label for="login.username">Username: <g:textField name="login.username"/><br/>
<label for="login.password">Password: </label>
<g:textField name="login.password"/><br/>
<label for="comment.text">Comment</label>
<g:textField name="comment.text"/><br/>
<br/>
<input type="submit"/>
</g:form>
class HomeController {
def index = { }
def save = { LoginCommand login, CommentCommand comment ->
println "login.username: ${login.username}"
println "login.password: ${login.password}"
println "comment.text: ${comment.text}"
redirect action: "index"
}
}
class LoginCommand {
String username
String password
}
class CommentCommand {
String text
}
try {
LdapContext context = new InitialLdapContext((Hashtable) [
(Context.INITIAL_CONTEXT_FACTORY): "com.sun.jndi.ldap.LdapCtxFactory",
(Context.PROVIDER_URL) : "ldap://ldap.localdomain.com",
(Context.SECURITY_AUTHENTICATION): "simple",
(Context.SECURITY_PRINCIPAL) : "DOMAIN\\invalid",
(Context.SECURITY_CREDENTIALS) : "invalid",
(Context.REFERRAL) : "follow",
(Context.BATCHSIZE) : "30"
], (Control[]) []);
println "Logged in!"
} catch (AuthenticationException e) {
println "NOT lOGGED IN"
}
@echo off
set GRAILS_HOME=C:/grails-1.3.4
set JAVA_OPTS=%JAVA_OPTS% -Divy.cache.dir=%CD%/target/libraries
set JAVA_OPTS=%JAVA_OPTS% -Dgrails.project.work.dir=target/work
%GRAILS_HOME%\bin\grails.bat %*
java.lang.NullPointerException
at grails.test.MockUtils.mockDomain(MockUtils.groovy:443)
at grails.test.MockUtils$mockDomain.call(Unknown Source)
at grails.test.GrailsUnitTestCase.mockDomain(GrailsUnitTestCase.groovy:131)
at grails.test.GrailsUnitTestCase.mockDomain(GrailsUnitTestCase.groovy:129)
package grails.test
import org.codehaus.groovy.grails.plugins.GrailsPluginManager
import org.codehaus.groovy.grails.plugins.PluginManagerHolder
class FixedGrailsUnitTestCase extends GrailsUnitTestCase {
protected void setUp() {
super.setUp();
PluginManagerHolder.pluginManager = [
hasGrailsPlugin: { String name -> true }
] as GrailsPluginManager
}
protected void tearDown() {
super.tearDown();
PluginManagerHolder.pluginManager = null
}
}
routes.MapPageRoute("Form", "Example", "~/Forms/Example.aspx");
routes.MapRoute(
"ActionWithFormat",
"{controller}/{action}.{format}"
);
routes.MapRoute(
"ActionWithFormatAndId",
"{controller}/{action}/{id}.{format}"
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new {
controller = "Home",
action = "Index",
id = UrlParameter.Optional,
format = "html"
}
);
public ActionResult Show(String id, String format)
{
var person = new { FirstName = "John", LastName = "Doe" };
switch (format)
{
case "json":
return Json(person, JsonRequestBehavior.AllowGet);
default:
return View(person);
}
}