Grails

pluginContextPath is not set for installed plugin

Details

  • Type: Bug Bug
  • Status: Closed Closed
  • Priority: Blocker Blocker
  • Resolution: Won't Fix
  • Affects Version/s: 1.2-RC1
  • Fix Version/s: 1.2.2
  • Component/s: None
  • Labels:
    None

Description

We have a plugin which provides a taglib for easily showing messages via some javascript-code.

One tag provides code for integrating the necessary code for loading the resources (css, js) for the page in the header. The resources are shipped with the plugin. We are calling g.resource for creating the path, see attached code.

In Grails <= 1.2-M4 this worked correctly using variable pluginContextPath.

However in 1.2-RC1 this has changed.

Instead of creating e.g. the path to the resource as "/plugins/my-tism-0.1/js/jquery.js" the path is now being rendered as "/js/jquery.js", because the pluginContextPath is null for some reason, which we verified using some debug code.

Looking somewhat further into the issue we found that pageScope.pluginContextPath is also null.

Activity

Hide
Thomas Heinen added a comment -

We have a second plugin which seems to work correctly and where the pluginContextPath has the correct value.

I am looking further into this. The code has not changed since 1.2-M4 for both plugins, so it still looks like something has changed in the framework.

Show
Thomas Heinen added a comment - We have a second plugin which seems to work correctly and where the pluginContextPath has the correct value. I am looking further into this. The code has not changed since 1.2-M4 for both plugins, so it still looks like something has changed in the framework.
Hide
Lari Hotari added a comment -
Show
Lari Hotari added a comment - Probably caused by this commit: http://github.com/grails/grails/commit/e5505ac0dde8cfbd8b9c3f72d9780bb3690efdde
Hide
Elisha Klein added a comment -

This plugin has this problem too:
http://grails.org/plugin/super-file-upload

Is there a workaround for this?

Show
Elisha Klein added a comment - This plugin has this problem too: http://grails.org/plugin/super-file-upload Is there a workaround for this?
Hide
Graeme Rocher added a comment -

I created a new plugin and added the attached tag library to the plugin project. When I run the plugin project the path is blank which is correct. When I think package it up and installed it into an application I get:

pluginContextPath is /plugins/myplugin-0.1

Which is also correct. I did have to adjust the tag library and Grails now implicitly puts in the paths when using the g.resource tag. So instead of:

attrs.base = g.resource( dir:"${pluginContextPath}/js/" )

This has to be:

attrs.base = g.resource( dir:"js/" )

However, other than that I don't see a bug here.

Show
Graeme Rocher added a comment - I created a new plugin and added the attached tag library to the plugin project. When I run the plugin project the path is blank which is correct. When I think package it up and installed it into an application I get:
pluginContextPath is /plugins/myplugin-0.1
Which is also correct. I did have to adjust the tag library and Grails now implicitly puts in the paths when using the g.resource tag. So instead of:
attrs.base = g.resource( dir:"${pluginContextPath}/js/" )
This has to be:
attrs.base = g.resource( dir:"js/" )
However, other than that I don't see a bug here.
Hide
Thomas Heinen added a comment - - edited

I can not confirm that your solution works for us.

I created a simple taglib for referencing javascript-files from our plugin and changed the line for setting base-attribute from attrs.base = g.resource( dir:"${pluginContextPath}/js" ) to attrs.base = g.resource( dir:"js" ) just as you suggested.

def javascript = { attrs, body ->
   def file = attrs.remove( "file" )
   attrs.base = g.resource( dir:"js" )
   attrs.src = g.resource( file:"${file}.js" )
   out << g.javascript( attrs, body )
}

On the page I use the taglib like this:

<mytism:javascript file="misc"/>

If pluginContextPath is null the output on the page is still "/js/misc.js" and not "/mytism-0.2/js/misc.js" as it should be, thus rendering the plugin useless, because we cannot access any resources from it.

As a workaround we are giving the absolute path in the taglib, but have to change this each time the plugin-version changes, which is really cumbersome:

def javascript = { attrs, body ->
   println "MyTISMGeneralTagLib/javascript pluginContextPath is ${pluginContextPath?:'null'}"
   def file = attrs.remove( "file" )
   def pCP = pluginContextPath ?: '/plugins/my-tism-0.2' // workaround for bug in 1.2
   attrs.base = g.resource( dir:"${pCP}/js" )
   attrs.src = g.resource( file:"${file}.js" )
   out << g.javascript( attrs, body )
}

I will try to put together an easy example and upload it soon, so you can reproduce the error and possibly fix it. Thanks.

Show
Thomas Heinen added a comment - - edited I can not confirm that your solution works for us. I created a simple taglib for referencing javascript-files from our plugin and changed the line for setting base-attribute from attrs.base = g.resource( dir:"${pluginContextPath}/js" ) to attrs.base = g.resource( dir:"js" ) just as you suggested.
def javascript = { attrs, body ->
   def file = attrs.remove( "file" )
   attrs.base = g.resource( dir:"js" )
   attrs.src = g.resource( file:"${file}.js" )
   out << g.javascript( attrs, body )
}
On the page I use the taglib like this:
<mytism:javascript file="misc"/>
If pluginContextPath is null the output on the page is still "/js/misc.js" and not "/mytism-0.2/js/misc.js" as it should be, thus rendering the plugin useless, because we cannot access any resources from it. As a workaround we are giving the absolute path in the taglib, but have to change this each time the plugin-version changes, which is really cumbersome:
def javascript = { attrs, body ->
   println "MyTISMGeneralTagLib/javascript pluginContextPath is ${pluginContextPath?:'null'}"
   def file = attrs.remove( "file" )
   def pCP = pluginContextPath ?: '/plugins/my-tism-0.2' // workaround for bug in 1.2
   attrs.base = g.resource( dir:"${pCP}/js" )
   attrs.src = g.resource( file:"${file}.js" )
   out << g.javascript( attrs, body )
}
I will try to put together an easy example and upload it soon, so you can reproduce the error and possibly fix it. Thanks.
Hide
Thomas Heinen added a comment - - edited

I put together the example now which shows that your solution is wrong.

I created a DemoTagLib:

DemoTagLib.groovy
class DemoTagLib {
   static namespace = 'demo'
   def out // to facilitate testing
   def grailsApplication

   def javascript = { attrs, body ->
      println "DemoTagLib/javascript pluginContextPath is ${pluginContextPath?:'null'}"
      def file = attrs.remove( "file" )
      attrs.base = g.resource( dir:"js" )
      attrs.src = g.resource( file:"${file}.js" )
      out << g.javascript( attrs, body )
   }
}

In the web-app/js directory I created a file:

test.js
// just some js-code
function checkAll( field ) {
   for ( i = 0; i < field.length; i++ ) {
      field[ i ].checked = true;
   }
}

In the main application I installed that plugin and put the following code in the layout:

<demo:javascript file="test"/>

The resulting rendered HTML-code for that looks like this:

<script type="text/javascript" src="/js/test.js"></script>

Which is wrong as the pluginContextPath is missing, thus the file is not being found.

Alas in that small demo I could not reproduce the pluginContextPath actually being null, thus also making the result invalid for the previous way the plugin was written, using variable pluginContextPath in the taglib. I am not sure how this can be reproduced, but it happens in our live-plugin and it is a blocker for us, when not using the ugly workaround I depicted above.

Show
Thomas Heinen added a comment - - edited I put together the example now which shows that your solution is wrong. I created a DemoTagLib:
DemoTagLib.groovy
class DemoTagLib {
   static namespace = 'demo'
   def out // to facilitate testing
   def grailsApplication

   def javascript = { attrs, body ->
      println "DemoTagLib/javascript pluginContextPath is ${pluginContextPath?:'null'}"
      def file = attrs.remove( "file" )
      attrs.base = g.resource( dir:"js" )
      attrs.src = g.resource( file:"${file}.js" )
      out << g.javascript( attrs, body )
   }
}
In the web-app/js directory I created a file:
test.js
// just some js-code
function checkAll( field ) {
   for ( i = 0; i < field.length; i++ ) {
      field[ i ].checked = true;
   }
}
In the main application I installed that plugin and put the following code in the layout:
<demo:javascript file="test"/>
The resulting rendered HTML-code for that looks like this:
<script type="text/javascript" src="/js/test.js"></script>
Which is wrong as the pluginContextPath is missing, thus the file is not being found. Alas in that small demo I could not reproduce the pluginContextPath actually being null, thus also making the result invalid for the previous way the plugin was written, using variable pluginContextPath in the taglib. I am not sure how this can be reproduced, but it happens in our live-plugin and it is a blocker for us, when not using the ugly workaround I depicted above.
Hide
Thomas Heinen added a comment - - edited

I looked deeper into this issue today and compared the plugin for our persistency framework called "MyTISM" to the demo-plugin I built for the bug report.

I stripped down our plugin to be exactly the same as the demo-plugin and did intermediate testing. Our plugin kept being broken. The demo works.

Then I realized that the camel-case might be the source of the problem and renamed the demo-plugin to "DeMONEW". As soon as I did that, the demo-plugin was broken, too!

I analyzed the Grails-Source and came across the root of the problem:

src/java/grails/util/GrailsNameUtils.java:74
private static String getClassNameForLowerCaseHyphenSeparatedName(String name) {
    // Handle null and empty strings.
    if (name == null || name.length() == 0) return name;

    if(name.indexOf('-') > -1) {
        StringBuilder buf = new StringBuilder();
        String[] tokens = name.split("-");
        for (String token : tokens) {
            if (token == null || token.length() == 0) continue;
            buf.append(token.substring(0, 1).toUpperCase())
                    .append(token.substring(1));
        }
        return buf.toString();
    }
    else {
        return name.substring(0,1).toUpperCase() + name.substring(1);
    }
}

This code is responsible for translating the hyphenated form of the plugin-name back to the original camel-case version of the plugin's class-name. This classname is later on being used for building the pluginContextPath.

The problem here is that this code assumes that only the first letter after a hyphen was a capital. This must not neccessarily be true, as this conversion from camel-case to hyphenated version obviously does not work like that. In our example the plugin is called "MyTISM" but results in the hyphenated string "my-tism", thus the conversion using the above function yields the wrong String "MyTism". This problem occurs every time a plugin name contains more than one capital in a row. As a consequence, the plugin is not found and the pluginContextPath is still null instead of being set to the actual path.

The same problem may occur at different places in the code where this conversion is done, e.g. src/java/org/codehaus/groovy/grails/commons/GrailsClassUtils.java:451 or at other places where the faulty results of the conversions may be utilized and may lead to further errors like this one.

Show
Thomas Heinen added a comment - - edited I looked deeper into this issue today and compared the plugin for our persistency framework called "MyTISM" to the demo-plugin I built for the bug report. I stripped down our plugin to be exactly the same as the demo-plugin and did intermediate testing. Our plugin kept being broken. The demo works. Then I realized that the camel-case might be the source of the problem and renamed the demo-plugin to "DeMONEW". As soon as I did that, the demo-plugin was broken, too! I analyzed the Grails-Source and came across the root of the problem:
src/java/grails/util/GrailsNameUtils.java:74
private static String getClassNameForLowerCaseHyphenSeparatedName(String name) {
    // Handle null and empty strings.
    if (name == null || name.length() == 0) return name;

    if(name.indexOf('-') > -1) {
        StringBuilder buf = new StringBuilder();
        String[] tokens = name.split("-");
        for (String token : tokens) {
            if (token == null || token.length() == 0) continue;
            buf.append(token.substring(0, 1).toUpperCase())
                    .append(token.substring(1));
        }
        return buf.toString();
    }
    else {
        return name.substring(0,1).toUpperCase() + name.substring(1);
    }
}
This code is responsible for translating the hyphenated form of the plugin-name back to the original camel-case version of the plugin's class-name. This classname is later on being used for building the pluginContextPath. The problem here is that this code assumes that only the first letter after a hyphen was a capital. This must not neccessarily be true, as this conversion from camel-case to hyphenated version obviously does not work like that. In our example the plugin is called "MyTISM" but results in the hyphenated string "my-tism", thus the conversion using the above function yields the wrong String "MyTism". This problem occurs every time a plugin name contains more than one capital in a row. As a consequence, the plugin is not found and the pluginContextPath is still null instead of being set to the actual path. The same problem may occur at different places in the code where this conversion is done, e.g. src/java/org/codehaus/groovy/grails/commons/GrailsClassUtils.java:451 or at other places where the faulty results of the conversions may be utilized and may lead to further errors like this one.
Hide
Graeme Rocher added a comment -

To be honest changing that conversion convention could have wide ranging consequences in other areas of the framework and I don't think changing it for a pretty minor restriction (the need to use pure camel case and not capitals) is worth the risk in breaking other areas.

My suggestion to your problem is simply to rename your MyTISM plugin to MyTism

Show
Graeme Rocher added a comment - To be honest changing that conversion convention could have wide ranging consequences in other areas of the framework and I don't think changing it for a pretty minor restriction (the need to use pure camel case and not capitals) is worth the risk in breaking other areas. My suggestion to your problem is simply to rename your MyTISM plugin to MyTism
Hide
Thomas Heinen added a comment -

Then maybe you should at least update the documentation for plugins accordingly, so that nobody else runs into that trap.

Show
Thomas Heinen added a comment - Then maybe you should at least update the documentation for plugins accordingly, so that nobody else runs into that trap.
Hide
Thomas Heinen added a comment -

One more thing:

In Grails <= 1.2-M4 all this worked correctly and AFAIK the conversion convention was not changed since. So something else must have changed which caused this effect. Maybe the way the pluginContextPath is determined?

I understand if you don't want to invest too much time into this issue, but it breaks compatibility for some plugins migrating from 1.1 to 1.2 so a hint on this in some upgrade-docu would be nice, too.

Show
Thomas Heinen added a comment - One more thing: In Grails <= 1.2-M4 all this worked correctly and AFAIK the conversion convention was not changed since. So something else must have changed which caused this effect. Maybe the way the pluginContextPath is determined? I understand if you don't want to invest too much time into this issue, but it breaks compatibility for some plugins migrating from 1.1 to 1.2 so a hint on this in some upgrade-docu would be nice, too.

People

Vote (4)
Watch (5)

Dates

  • Created:
    Updated:
    Resolved: