Middleware for the REST of us

bea_think_oracleI’m sitting in my third Oracle Fusion Middleware briefing, this one at the Willard Hotel in Washington, DC. Thomas Kurian has been going through all the products in the Oracle stack in excruciating detail.

First let me say this: Thomas Kurian is a really smart guy. He holds an BS in EE from Princeton summa cum laude (that’s Latin for really fucking good). He holds an MBA from the Stanford GSB. He’s been working for Oracle forever and he even knows how to pronounce Fuego (FWAY-go). I’m dutifully impressed.

Unfortunately, all those academic credentials and 10+ years in the industry is barely the minimum requirement for getting your head around the middleware space. Either I don’t have enough (0) letters after my name, or I just don’t get it.

For starters, there are way too many products — the middleware space is filled with “ceremonious complexity” (to quote Neal Ford). App servers, data services layers, service buses, web service producers and consumers — even portals, content management and collaboration has been sucked into this space. Don’t get me wrong: the goals of the stack are admirable — middleware tries to glue together all the heterogeneous, fragmented systems in the enterprise. Everyone knows that most enterprises are a mess of disparate systems and they need this glue to provide unified user experiences that hide the complexity of these systems from the people who have to use them. That makes the world a better place for everybody.

That was also, not coincidentally, one of Plumtree’s founding principles and the concept — integrating enterprise systems to improve the user experience — has guided my career since I got my lowly undergraduate degree in Computer Science from Stanford in 1998.

So, it’s a good concept, however, if you’re considering middleware because you’re trying to clean up the mess that your enterprise has become, you need to ask yourself the following fundamental question:

Does middleware add to or subtract from the overall complexity of your enterprise?

Your enterprise is already insanely complicated. You’ve got Java, .NET, perhaps Sharepoint, maybe an enterprise ERP system like SAP and say, an enterprise open source CRM system like SugarCRM or a hosted service like SalesForce.com. The bleeding edge IT folks and even (god forbid) people outside of IT are installing wikis written in PHP (e.g. MediaWiki) along with collaborative software like Basecamp written in Ruby on Rails. I’m not even going to mention all the green-screen mainframe apps still lurking in the enterprise — wait, I just did. This veritable cornucopia of systems just scratches the surface of what exists at many large — and even some mid-to-small-sized companies — today.

So clearly there’s a widespread problem. But what’s the solution?

At the end of his impressive presentation, I asked Thomas the following question:

“How can middleware from Oracle/BEA help you make sense of the fragmented, heterogeneous enterprise when you have existing collaborative (web 2.0) technologies written in PHP, Ruby on Rails, etc. running rampant throughout IT and beyond?”

(Okay, so I wasn’t exactly that pithy, but it was something close to that.)

His Aladdin-esque answer came in the form of three choices:

    1. “Take control of” and “centralize” your IT systems by replacing everything with Oracle Web Center spaces
    2. Ditto by migrating everything to UCM (Stellant)
    3. Build a services framework and aggregate everything in one of four ways:
        1. Use a Java transaction layer (JSR 227)
        2. Use a portlet spec like JSR 168 or WSRP
        3. Build RESTful web services
        4. Use the WebPart adapter for Sharepoint

      I like to call answers one and two “The SAP Approach.” In other words, we’re SAP, we’re German, wir geben nicht einen Scheiße about your existing enterprise software, you’re now going to do it the SAP way (or the highway).

Will companies buy into that? Some companies may. Many will not. ERP is a well understood space, so this approach has worked for SAP. Enterprise 2.0 is not terribly well understood, so that means even more diversity in the enterprise software milieu.

So the only approach that I believe in is #3: integrate. Choose the right tool for the right problem, e.g. the WebPart adapter if you’re using Sharepoint. Use REST when appropriate, e.g. when you need a lightweight way to send some JSON or XML across the wire between nonstandard or homegrown apps. Use JSR 168/286 for your Java applications. Even use SOAP if the backend application already supports it.

Keep things loosely coupled so that you can plug different components in and out as needed.

This requires a lot of development — the glue — but, I don’t think there’s any way around that. (You should take that with a grain of salt, because my company has been supplying the government and the commercial world with exactly that kind of development expertise since 2002.)

As for the overarching, user facing “experience” or “interaction” product — that’s where I’ve always used Plumtree (or AquaLogic Interaction).

Will I start using Web Center Spaces? At this point, I’m still not sure.

If it can be used as the topmost bit of the architectural stack to absorb and surface all the enterprise 2.0 software that my customers are running, then perhaps. If it’s going to replace all the enterprise software that my customers are running, then no way JosΓ©.

This conundrum really opens up a new market for enterprise software: I call it “Middleware for the REST of us” or MMM (not M&M, 3M or M3, because they’re already taken): “Mid-Market Middleware” — similar to the way 37signals approaches (with a great deal of hubris and a solid dose of arrogance) the “Fortune Five Million” by marketing their products toward the whole long-tail of small and medium-sized companies. Maybe the world needs a RESTful piece of hardware that just aggregates web services and spits out a nice UI, kind of like the “Plumtree in a Box” idea that Michael Young (former Plumtree Chief Architect, now Chief Architect at RedFin) had back in the last millennium.

Oracle Web Center Spaces might be the right choice for some very large enterprises, but what about the REST of us?

Write an ALUI IDS in Under 15 Lines Using Ruby on Rails

Not only is it possible to write an ALUI Identity Service in Ruby on Rails, it’s remarkably easy. I was able to do the entire authentication part in fewer than 15 lines of code! However, I ran into problems on the synchronization side and ended up writing that part in Java. Read on for all the gory details.

As part of building the suite of social applications for BEA Participate 2008, we’re designing a social application framework in Ruby on Rails and integrating it with ALI 6.5. Not being a big fan of LDAP, I decided to put the users of the social application framework in the database (which is MySQL). Now, when we integrate with ALI, we need to sync this user repository (just as many enterprises do with Active Directory or LDAP).

So I set out to build an IDS to pull in users, groups and memberships in Ruby on Rails.

It’s pretty obvious that Ruby on Rails favors REST over SOAP for their web service support. However, they still support SOAP for interoperability and it mostly works. I did have to make one patch to Ruby’s core XML processing libraries to get things humming along. I haven’t submitted the patch back to Ruby yet, but at some point I will. Basically, the problem was that the parser didn’t recognize the UTF-8 encoding if it was enclosed in quotes (“UTF-8”). This patch suggestion guided me in the right direction, but I ended up doing something a little different because the suggested patch didn’t work.

I changed line 27 of lib/ruby/1.8/rexml/encoding.rb as follows:

 enc = enc.nil? ? nil : enc.upcase.gsub('"','') #that's a double quote inside single quotes

Now that Ruby’s XML parser recognized UTF-8 as a valid format, it decided that it didn’t support UTF-8! To work around this, I installed iconv, which is available for Windows and *nix and works seamlessly with Ruby. In fact, after installation, all the XML parsing issues went bye-bye.

Now, on to the IDS code. From your rails project, type:

ruby script/generate web_service Authenticate

This creates app/apis/authenticate_api.rb. In that file, place the following lines of code:

class AuthenticateApi < ActionWebService::API::Base
 api_method :Authenticate, :expects => [{:Username =>
:string}, {:Password =>
:string}, {:NameValuePairs =>
[:string]}], :returns =>
[:string]
end

All you’re doing here is extending ActionWebService and declaring the input/output params for your web service. Now type the following command:

ruby script/generate controller Authenticate

This creates the controller, where, if you stick with direct dispatching (which I recommend), you’ll be doing all the heavy lifting. (And there isn’t much.) This file should contain the following:

class AuthenticateController < ApplicationController
 web_service_dispatching_mode :direct
 wsdl_service_name 'Authenticate'
 web_service_scaffold :invoke

 def Authenticate(username, password, nameValuePairs)
   if User.authenticate(username, password)
     return ""
   else
     raise "-102" #generic username/password failure code
   end
 end
end

Replace User.authenticate with whatever mechanism you’re using to authenticate your users. (I’m using the login_generator gem.) That’s all there is to it! Just point your AWS to http://localhost:3000/authenticate/api and you’re off to the races.

Now, if you want to do some functional testing (independently of the portal), rails sets up a nice web service scaffold UI to let you invoke your web service and examine the result. Just visit http://localhost:3000/authenticate/invoke to see all of that tasty goodness.

There you have it — a Ruby on Rails-based IDS for ALUI in fewer than 15 lines of code!

The synchronization side of the IDS was almost just as simple to write, but after countless hours of debugging, I gave up on it and re-wrote it in Java using the supported ALUI IDK. Although I never could quite put my finger on it, it seemed the problem had something to do with some subtleties about how BEA’s XML parser was handing UTF-8 newlines. I’ll post the code here just in case anyone has an interest in trying to get it to work. Caveat: this code is untested and currently it fails on the call to GetGroups because of the aforementioned problems.

In app/apis/synchronize_api.rb:

class SynchronizeApi < ActionWebService::API::Base
 api_method :Initialize, :expects =>
[{:NameValuePairs =>
[:string]}], :returns =>
[:integer]
 api_method :GetGroups, :returns =>
[[:string]]
 api_method :GetUsers, :returns =>
[[:string]]
 api_method :GetMembers, :expects =>
[{:GroupID => :string}], :returns =>
[[:string]]
 api_method :Shutdown
end

In app/controllers/synchronize_controller.rb:

class SynchronizeController < ApplicationController
  web_service_dispatching_mode :direct
  wsdl_service_name 'Synchronize'
  web_service_scaffold :invoke

  def Initialize(nameValuePairs)
    session['initialized'] = true
    return 2
  end

  def GetGroups()
    if session['initialized']
      session['initialized'] = false
      groups = Group.find_all
      
      groupNames = Array.new
      for group in groups
        groupNames << "<SecureObject Name=\"#{group.name}\" AuthName=\"#{group.name}\" UniqueName=\"#{group.id}\"/>" 
      end 
      return groupNames
    else
      return nil
    end
  end
  
  def GetUsers()
    if session['initialized']
      session['initialized'] = false
      users = User.find_all
      
      userNames = Array.new
      for user in users
        userNames << "<SecureObject Name=\"#{user.login}\" AuthName=\"#{user.login}\" UniqueName=\"#{user.id}\"/>" 
      end
      
      return userNames
    else
      return nil
    end
  end

  def Shutdown()
    return nil
  end
end

Comments

Comments are listed in date ascending order (oldest first)

  • Nice post, Chris. This is the first time I’ve seen this done!

    Posted by: dmeyer on January 20, 2008 at 4:16 PM

  • Thank you, David.I just noticed that part of my sync code was chomped off in the blog post because WordPress was assuming that was actually an opening HTML/XML tag. I made the correction so the above code now accurately reflects what I was testing.

    Posted by: bucchere on January 21, 2008 at 1:16 PM

Four ALI IDKs at your Disposal — Fifth One on the Way?

As most of you already know, there are four IDKs out there in IDK-land. To take a step back, if you’re really new to ALUI (formerly Plumtree) development, you can read about how ALUI handles portlets and how an IDK helps you write portlets. So, back to the four IDKs. They are all freely-downloadable from BEA — I’ve included links here:

*These two IDKs were written by developers at bdg and then released to the open source community on dev2dev’s CodeShare.

edocs warns you in boldface type: always use the IDK. That’s sound advice, given that the IDK API isolates you, as a portlet developer, from changes to the underlying protocol that ALUI uses to communicate between the portal and portlets (CSP). However, what if you’re not writing a portlet in Java, .NET, Ruby or PHP? As an aside, once upon a time, there were Perl and ColdFusion GDKs (predecessor to the EDK/IDK), but these development kits are no longer maintained by anyone, although I know for a fact that the ColdFusion GDK is still in use by an ALUI customer because it came up during a sales call!

So, back to my “what if” question: how can you write a portlet without an IDK? It’s actually not too hard if you’re just getting information from the portal. However, it gets challenging when you start setting preferences and then it starts to get really painful when you start dealing with Unicode issues, encrypted settings and some of the other really hairy stuff. So that’s why edocs implores you to use an IDK, when one is available.

If you do go down the road of writing a portlet in a language where an IDK isn’t available, I highly recommend that you at least abstract out your CSP calls such that they’re isolated from the rest of your portlet code. While you’re at it, you might as well follow the same API that BEA uses; in other words, write your own IDK (or at least the parts of it that you need to get your portlet done). To get you started, here’s how BEA probably implemented one of the IDK methods in Java:

public AggregationMode getAggregationMode() {
  if (!request.isGatewayed()) {
    throw new NotGatewayedException("Request not gatewayed.");
  }
  
  if (request.getHeader("CSP-Aggregation-Mode").equals("Multiple"))
    return AggregationMode.Multiple;
  } else {
    return AggregationMode.Single;
  }
}

So, go forth and write your own IDK. Or, preferably, ask a bdg-er to write one for you!

On a related note, one of our customers recently asked us to build an IDK for Lotus Notes/Domino in LotusScript. We’re trying to figure out if other people might be interested in this IDK so that we can decide if we’re going to open source it or do it as a consulting project (or some hybrid of the two). If you are interested in LN/Domino development for ALUI, let us know by commenting on this blog. I always love hearing feedback from users of the PHP and Ruby IDKs as well.

Comments

Comments are listed in date ascending order (oldest first)

  • Hi Chris. We have a couple of remote portlets on web servers running Perl so I looked around for awhile for the original Perl IDK but couldn’t locate it, so I’ve written a Perl module that supports the methods provided by IPortletRequest. They’re read-only methods but are really all we needed access to. man-page is here…
    http://webdev.co.nz/Perl/IPortletRequest.txt Is this worth placing in codeshare do you reckon? I don’t see a lot of action in the forums concerning Perl use.

    Dean Stringer ([email protected])

    Posted by: deeknow on March 22, 2007 at 2:00 PM

  • Hello Dean! The original Perl GDK was written so long ago that I’m not sure how much good it would do. It was based on CSP 1.0 and it followed the old GSServices API rather than the new com.plumtree.remote.portlet API.

    I’m pretty stoked to find out that someone else has written at least part of an IDK — that’s impressive. Do I think it’s worth posting to CodeShare? I haven’t heard much talk about ALUI and Perl, but I guess it can’t hurt.

    Posted by: bucchere on March 22, 2007 at 5:03 PM

  • Hi, Chris. We would love to have a Lotus Notes/Domino IDK available to us, as we have lots of “legacy” applications in Domino that we want to expose in ALUI, and an IDK would certainly help! Just my nickel’s worth… πŸ™‚

    Posted by: kcepull on March 30, 2007 at 11:21 AM

  • Thanks for your comment. If you don’t mind my asking, would you be willing to pay for such a thing or is it only a nice-to-have that you would download and use if it were opensource, but not pay for it if it were a commercial product?

    Posted by: bucchere on March 31, 2007 at 6:07 PM

WLP + Adrenaline = ALI?

I recall sitting in a meeting in 1998 where we were discussing how to aggregate portlet content into a portal page. We talked a lot about iframes but couldn’t consider them as a serious integration option because of security, scalability/performance, caching and portal-to-portlet communication. Instead, we spent the next year building and testing the HTTPGadgetProvider, which later came to be called the “(Massively) Parallel Portal Engine.” (The term “Massively” was later dropped and I believe the name “Parallel Portal Engine” or PPE for short finally stuck.) I won’t go into details about how the PPE works, but if you’re interested, you can check out this great page in edocs that sums it up nicely.

So anyway, iframes are certainly reasonble way to build a portal in a day. But, in terms of building a robust enterprise portal that can actually withstand the demands of more than say, ten users, and that will pass even the most rudimentary security evaluation, iframes are complete nonsense.

So, today, during my lunch break, I attended Peter Laird’s Webinar, which he advertised in his nascent blog. It was all about enterprise mashups, a topic by which I’m very much intrigued. (Recall that PTMingle, my winning entry in the 2005 Plumtree Odyssey “Booth of Pain” coding competition was a mashup between Hypergraph, Google Maps, del.icio.us and Plumtree User Profiling.)

Imagine my surprise when Peter described how you can mash up Google “Gadgets” and other resources available via URLs using Adrenaline, a “new” technology from the WLP team based on, of all things, iframes. It was like entering a worm hole and being transported back to 1998. (I was single again, I had no kids, I was thinner and I had more hair on my head . . . and less on my back.) But the weird thing about this parallel universe is that BEA engineers were telling me that iframes were a great way to mashup enterprise web content and that intranets all over the world could benefit from this revolutionary concept. Intranets? You mean the things that everybody replaced with portals in the last millennium? Iframes? I must have been dreaming . . . .

When I finally came back to my senses, a few things occurred to me.

First of all, it’s 2007. Portals are a thing of the past. For some of us, that will be a hard pill to swallow. But let’s face it, innovators have moved on to blogging, wikis, tagging/folksonomies and lots of other nice web 2.0 sites that all have rounded corners. The bleeding edge folks have decided that many is smarter than any. The rest of the world will catch up soon.

Secondly, if you are still building a portal or composite application of any flavor, iframes are not a viable solution. They fall short in the following ways:

Portal-to-Portlet Communication

Say you want to send information (like the name of the current user) down to a portlet running in an iframe. Hmmm, the request for an iframe comes from the browser, not from the portal. So, if anything needs to be passed into the iframe, I guess you have to put in in the URL in the request for the iframe. That’s great, but that URL is now visible in the page’s source. So a simple, “Hello [your name]” portlet where the portlet gets the name from the portal is doable. But what about passing a password? That information would need to go first to the browser and then back to the remote tier, which, from a security standpoint, is a complete showstopper.

Security

Let’s talk a little more about security. Since you’re using an iframe, the requests aren’t proxied by the portal. Instead, a page of HTML gets sent from the portal to the browser and then the browser turns around and makes requests to all the iframes on that page. Since the portal isn’t serving as a proxy, it can’t control what you do and don’t have rights to see, so security is completely thrown out of the window. (Or should I say, thrown out of the iframe?) Moreover, in an enterprise deployment, the portal usually sits in the DMZ and proxies requests out to bits and pieces of internal systems in order to surface them for extranet users. If you’re using iframes, every bit of content needs to be visible from an end user’s browser. So what’s to stop an end-user from scraping the URL out of a portal page and hitting a portlet directly? Nothing! (If I understand what I’m reading correctly, the WLP team is calling this a feature. I would call it a severe security risk.)

Scalability/Performance

Yes, this approach will work for Google Gadgets. But Google has more money than pretty much everyone. They can afford to spend frivolously on anything, including hardware. However, the rest of the world actually cares about the kind of load you put on a system when you create a “mashup.” A page consisting of five iframes is like five users hitting the sites with five separate requests, separate sessions and separate little “browsers.” If any of the iframes forces a full-page refresh or if the user does the unthinkable and say, moves to another page, every request is reissued and the mashup content is regenerated. This simply does not scale beyond a few users, unless you have as much money and as much hardware as Google does.

Caching

A properly designed portal or content aggregation engine will only issue requests to portlets when necessary. In other words, each remote portlet will only get a request if it needs to be loaded because the portal doesn’t have a cached entry. Unfortunately, you can’t do this with iframes because the portal doesn’t even know they exist. (Remember, all requests for iframe content go directly from the browser to the remote content, bypassing the portal entirely.)

What baffles me is why a company would acquire another company with a revolutionary technology (the PPE) and then start from ground zero and build a technology that does the same thing but without a portal-to-portlet communication model (preferences), security, scalability or caching. If consumers weren’t already confused, now they most certainly are.

As technologists, I hope you can see through the hype about Adrenaline and consider a product that actually allows you to mash up web content in a scalable and secure way and has been doing so since 1999. It’s called AquaLogic Interaction and it’s sold by a company we all know and love called BEA.

Comments

Comments are listed in date ascending order (oldest first)

  • I just discovered that the BID/AquaLogic (formerly Plumtree, Fuego, Flashline, etc.) folks are having another webinar, entitled “Harnessing Enterprise Mash-ups with Security and Control.” This webinar (I hope) will show:
    1. how ALI has been handling mashups since before mashups was even a buzzword and
    2. how Project Runner enables next generation mashups that allow you to invoke back-end applications and provision security, branding, SSO, etc. without actually funneling everything through the portal.

    If you were at today’s webinar and you’re now wondering how to do mashups with more robustness and security, then I hope you’ll attend this webinar. By all means, it’s just the responsible thing to do in order to offer customers different integration options when creating their mashups.

    Posted by: bucchere on January 10, 2007 at 7:31 PM

  • I’d like to add a couple points of clarity from BID product management. First of all, we’re happy to have passionate developers, but I fear this post may give the wrong impression about some of BEA’s technology and plans.

    WLP Adrenaline, ALUI, and project Runner are all complementary technologies that have a very exciting future when applied to problems such as Enterprise Mashups. You’ll be hearing more about them from BEA over the coming months through various venues, including Webinars targeted at WLP-specific use cases (such as Peter’s excellent talk) and ALUI use cases (including tomorrow’s Runner Webinar). There will also be the usual blogging and other activities.

    Just as WLP and ALUI product teams are aligned, these different technologies are aligned. Adrenaline offers WLP customers a way to extend their reach in fundamentally new ways, and Peter will expound on some technical subtleties to address some of Chris’ concerns. Runner, too, is very exciting, enabling a completely different set of use cases. As the details unfold we’ll demonstrate how well aligned these technologies are — just wait until you see them working together!

    – David Meyer

    Posted by: dmeyer on January 10, 2007 at 10:41 PM

  • Just for those that don’t know about Adrenaline, here’s an article introducing Adrenaline.

    Posted by: jonmountjoy on January 11, 2007 at 12:19 PM

  • Chris,

    As David writes, BEA is moving ahead with multiple approaches to address the enterprise mashup space. My webinar covered the approach WLP is taking, and in no way implied that ALUI is not also a viable player in this space. We offer our customers a choice of products, and different products make sense to different customers.

    As for the specific issues you raised:

    ** Technical Reply

    Good technical points, but I think you overemphasized the role of iframes within WLP. Let me cover the two places we showed the use of iframes:

    Use Case 1: injecting a portlet into a legacy webapp

    Demo: An iframe was used in the demo to inject a portlet into a legacy static html page with almost no modification to that page (one line change).

    WLP does support an alternative approach – an Ajax streamed portlet. I simply did not have time to demo it. Also, this is not a portal use case for including external non-portal content into a Portal; instead it is the inverse, which is to publish existing portal content into legacy web applications . It was intended to show a very inexpensive way to energize a dated application until it is rationalized into a portal. The focus here is on minimizing cost of supporting legacy, while building portlets in transit to a portal solution.

    Use Case 2: WLP as a Mashup composition framework

    Demo: Iframes were used to pull in non-WSRP capable components (e.g. Google Gadgets) onto a WLP page

    First, as background info, the WLP architecture supports the rendering of various types of portlets:

    • Local portlets (deployed within the webapp, JSF, JPF, etc)
    • WSRP portlets – an advanced remoting approach which handles security, inter-portlet communication, etc…
    • Iframe portlets – an available remoting approach
    • WLP partners with Kapow for remote clipped portlets (similar to the ALUI approach)

    In regards to this use case, you brought up specific concerns:

    Security

    Concerns about shared authentication were noted in my talk. If components come from outside the enterprise, there is no easy solution to that problem, regardless of what product you are using. However, I spoke of a couple approaches in the webinar, including SAML.

    If those components come from inside the enterprise, the security hacks you were referring to are generally not necessary. Our customers that expect SSO have a web SSO solution (typically, cookie powered, not password in the URL powered) in place within the enterprise.

    Caching/Performance

    The most serious concerns of yours appear to be performance related. Specifically, the concern is that a full page refresh of a page that contains N number of iframes will cause an N+1 number of requests. To expand on your concern, I will add that this is not only seen in pages with iframes, but also pages that use Ajax to pull in data. I would say that there are several reasons why this does not invalidate WLP’s approaches:

    1. Mashup pages with lots of iframe portlets approach

    Google Personalized Home Page makes use of iframes to implement their mashup framework. Many of the Gadgets on the page are rendered with an iframe. But you are mistaken in saying that this scales because Google is throwing tons of hardware at the problem. The iframe Gadgets rendered in GPHP are rendered not by Google, but by 3rd party gadget hosting servers around the world. Google does NOT have to process those iframe Gadget requests, it is a distributed approach. Likewise, you could create a WLP page where most of the portlets are iframe portlets that hit a distributed set of servers, if that makes sense. Or…

    2. Mashup pages with a mixture of portlets

    The 2nd demo in my webinar wasn’t showing a page with all iframe portlets. Rather, what the demo was showing was a WLP page with a couple of iframe portlets mixed in with local portlets. As shown above, WLP supports a number of portlet types, and a good approach is to build pages that are a mixture of that set.

    3. Ajax helps minimize page refreshes

    Your concern about iframe performance stems from the case in which the entire page refreshes. With the usage of Ajax becoming common, plus with WLP 9.2 built in support for auto-generating Ajax portlets, this impact can be minimized. Page refreshes are becoming more rare. With WLP 10.0, which releases in a few months, the Ajax support has been expanded to support Ajax based portal page changes, further reducing the liklihood of a page refresh.

    4. The “Bleeding Edge” guys are also using browser based mashup approaches

    You referred to the “Bleeding Edge” technologists in your blog as the people that are doing things correctly. What are they doing? Some of the time, those guys are doing browser based Mashups. They often use a combination of iframes and Ajax from the browser to implement their mashups. So the same approach that you dislike is already in common use across the web.

    ** Market Reply

    You state “Portals are a thing of the past”. An interesting opinion, but just that. IT cannot afford web sprawl, and so a framework for rationalization will always be in demand whether you call it a Portal or something new.

    New technologies continue to provide alternatives to existing methodologies and portals are no different. However, one thing that has distinguished portal frameworks is their ability to embrace new technologies. Struts, WSRP, JSF are all examples of this as are the Web 2.0 constructs like mashups and rich interfaces based on Ajax. This is all good news as the enterprise has a wealth of options to choose from.

    Posted by: plaird on January 11, 2007 at 2:56 PM

  • I must say, as a customer and developer, it’s great plumtree (I mean BEA, or is it Oracle) management allows you guys to express your own opinions. It so happens I’ve spent quite a bit of time trying to get JBoss Seam (and Ice Faces) to work with Aqualogic 6.1. I’ve been looking at the IFrame route, because the gateway stuff just isn’t working (it doesn’t properly rewrite the URLs for the Ajax stuff). I’ve come to hate the gateway. I bet it was a great idea before Ajax, but now it seems like almost every web 2.0 application is incompatible (needs major modification to get it to work). Or maybe I just don’t understand how to get it to work. Is there any good documentation on it? I’m hoping for some major improvements when 6.5 comes out though.

    Posted by: cmann50 on April 4, 2008 at 2:28 PM

How to Integrate PKI Certs or CAC Cards with ALI

In his 1947 speech to the House of Commons, Winston Churchill quipped, “It has been said that democracy is the worst form of government except all those other forms that have been tried.”

I’m not nearly as pithy as Sir Winston (nor as portly — at least not yet), but yet I feel the same way about passwords being used to protect web sites or other enterprise systems. In many ways, they’re the worst form of security out there except for everything else that’s been tried. Part of this has something to do with what I’ve coined Bucchere’s Axiom of Strong Passwords, which is a derivative of Murphy’s Law (which states that whatever can go wrong will). It goes something like this: the stronger a password is, the easier it is to hack. Why? Because if you force users into using a strong password, they’re more likely to write it down. And writing a password down defeats its purpose entirely.

The bottom line: passwords suck. But they’ve become the de-facto standard because they’re easier and cheaper than everything else we’ve tried, including PKI certs, biometrics (e.g. fingerprints, retina-scans), CAC cards, RSA secure IDs, etc. (Even for a cert-based authentication scheme, you still need a key to generate your cert, which is essentially just a glorified password.)

Just because passwords are the de-facto standard for authentication does not mean that we should quit trying to use other, ostensibly better forms of security, especially if 1) you’re protecting particularly sensitive data, 2) you’re open to the internet and 3) you have the resources (e.g. $$$) to invest in more robust forms of security. And I’m not talking about just buying an SSL cert from Verisign and continuing to have your users write down their passwords on post-it notes attached to their monitors. (Note to self: remove the post it note on your monitor with your password on it when you get back to the office.) I’m talking about using some sort of “soft” cert (e.g. PKI) or “hard” cert (e.g. CAC) to protect your system and your data.

Now if your system is ALI (formerly known as Plumtree Foundation or Plumtree Portal), you’re in luck, because the eggheads at what was once known as Plumtree have made this particularly easy to do. In fact, the hardest part is just getting the user’s identity out of the cert (see below the code snippet for some suggestions). Once you’ve done that, just drop a class into a jar that implements the ISSOProvider interface. (For those of you running on Windows, please don’t ask me to “port” this to C# — just take the Java code, drop it into Visual Studio.NET and then fix the syntax errors.)

But wait, SSO stands for “Single Sign On,” right? And what you’re really doing here is passing credentials from a cert to Plumtree and that has little or nothing to do with SSO. That’s a true statement. The subtlety here is that ISSOProvider, while it contains the letters SSO in its name, can be used for pretty much any form of authentication, whether you are using an SSO product or not.

CertIntegration.java

package com.bdgportal.alui.auth;

import com.plumtree.openfoundation.util.*;
import com.plumtree.openfoundation.web.*;
import com.plumtree.portaluiinfrastructure.sso.*;

public class CertIntegration implements ISSOIntegration {
 
   private XPHashtable settings;
 
   public CertIntegration() {
     ;
   }
 
   public boolean Initialize(XPHashtable settings) {
     this.settings = settings;       
     //String exampleSetting = ((XPArrayList)settings.GetElement("SettingName")).GetElement(0);
   }

   public String GetSSOProductName() {
     return "My Favorite Cert Integration";
   }

   /**
    * Gets the username from the cert and returns it to Plumtree. This will fail if the username
    * does not have a matching account in Plumtree. This can be a Plumtree database user or a user
    * imported from an authentication source, in which case you need to include the auth source
    * prefix in the username, e.g. "MyAuthSource/cbucchere"
    *
    * @param request The wrapped HttpServletRequest from the web container.
    * @return The object passed back to Plumtree for authentication with the portal.
    */
   public SSOLoginInfo GetLoginInfo(IXPRequest request) {
     String userName = ((XPRequest)request).GetUnderlyingObject().getUserPrincipal().getName();
     return new SSOLoginInfo(userName);
   }

   public String[] GetSecureCookies() {
     return null;
   }

   public String[] GetSecureHeaders() {
     return null;
   }

   public boolean OnLogout(IXPResponse response, String returnURI) {
     return false;
   }   
}

The hardest part about all this, as I said above, is getting the user name out of the PLI cert/CAC card/retina scan/etc. In the example above, I made MANY assumptions. First, I assumed that your portal is running on Weblogic, which understands and correctly implements Principal, which is a Java Servlet’s way of knowing who’s using it. Weblogic lets you plug custom implementations of the Principal class into its security infrastructure. All you need to do is extend java.security.Principal and then walk through a bunch of magical configuration steps to enable it.

Speaking of magical configuration, I neglected to mention that there are two small configuration steps that you need to perform in order to get your shiny new ISSOIntegration working in ALI. In portalconfig.xml, you need to set the value of SSOVendor setting to 100 (or greater) and then set the CustomSSOClass to the fully qualified name of the class you wrote that implements ISSOIntegration. For our Java example above, that would be com.bdgportal.alui.auth.CertIntegration and for .NET, it would the the name of your C# class.

Speaking of .NET . . . as many of you know, it is an entirely different animal with its own way of provisioning security to web applications (e.g. System.Web.Security).

Regardless of your platform, you need to get the user name out of whatever authentication method you’re using. Once you’ve accomplished that, just drop the code above into your project and replace the getUserPricipal().getName() with whatever mechanism you can find for getting your users’ names.

Assuming you trust your authentication mechanism to return the appropriate user name, you’ll have users getting logged into the portal via pretty much however you would like — CAC, PKI, biometrics, etc.

If only implementing a democracy were this easy . . . .

Comments

Comments are listed in date ascending order (oldest first)

  • This is wonderful article. How ever I’ve researched for a long time but still can not figure out what to do with Bea Weblogic to use Costom Identify Assertion. I wish this artical to have link to the document of how to “do the magical configuration steps”.

    Posted by: minh.tran on January 9, 2007 at 9:04 AM

  • This article was intended to be application server independent, but if you’re using BEA WebLogic, there’s a great article on how to set up custom identity providers which should work with this ALUI SSO solution.

    Posted by: bucchere on January 10, 2007 at 6:44 PM

  • NOTE: 1. the user’s password in the portal must be empty string. 2. jar should be put in portal.war and lib/java.

    Posted by: luotuoci on April 28, 2007 at 8:31 PM

A Slick Alternative to pt:standard.openerlink

For one reason or another your bosses (we all have more than one, don’t we?) have told you that the look-n-feel of the common object opener in ALUI just doesn’t cut it. Even though it’s powerful, scalable and pretty nice-looking and it includes a myriad of options (e.g search, browse, single vs. multi-select, set previously selected, etc.), they just want something different. Perhaps they don’t want a pop-up window. Perhaps they don’t like how many clicks it takes to get down to an object. Perhaps they’re just being difficult.

Regardless, you’ve been asked to come up with a clean, fast, in-place object selector that still shows a hierarchical view. (For the purposes of this discussion, I’m going to use the example of communities from here on out instead of just talking about “objects.”) So naturally, as a portlet developer, you turn to the IDK. Unfortunately, if you want to get portal metadata, you have to use the PRC/SOAP server. There goes fast. So maybe you can write it in native code or using database calls. There goes clean.

Your best bet here — and really the only good way to accomplish this — is to develop a custom taglib. Custom taglibs are quickly becoming my favorite new feature in G6. (BTW, if you aren’t on G6, upgrade ASAP — it’s worth the effort.) So, for your benefit, I decided to try my hand at writing a taglib to present a nice hierarchy of communities. Here’s what I discovered.

First off, let’s talk about the HTML I want to display in my custom tag for a moment. Whoever came up with the concept of select boxes and optgroup elements was a complete goofball. Why develop something that’s naturally suited for a hierarchy and then limit the hierarchy to a depth of one?

Here’s an example:

So I had to throw my initial idea of using nested option elements out the window simply because you can’t nest an optgroup within an option. Bummer.

So here’s the display I settled on:

There’s still a hierarchy here, it’s just flattened and there’s essentially a “breadcrumb” for each community. In the example I have, bdg is a top level community and services is a subcommunity of bdg. Consulting, development, integration and training are all subcommunities of services.

Alrighty then, so how to you construct this nice select box? And BTW, make it easy, clean and fast. Here you go:

package com.bdgportal.alui.taglib

import com.plumtree.openlog.OpenLogService;
import com.plumtree.openlog.OpenLogger;
import com.plumtree.portaluiinfrastructure.tags.*;
import com.plumtree.portaluiinfrastructure.tags.metadata.*;
import com.plumtree.xpshared.htmlelements.*;
import com.plumtree.server.*;

public class CommunitySelector extends ATag
{
   private static OpenLogger log = OpenLogService.GetLogger(
    OpenLogService.GetComponent("UI_Infrastructure"),
    "com.bdgportal.alui.taglib.CommunitySelector");

 public static final ITagMetaData TAG;
 public static final RequiredTagAttribute SELECT_ID;
 public static final RequiredTagAttribute SELECT_NAME;
 public static final OptionalTagAttribute SELECT_CLASS;
 public static final RequiredTagAttribute ROOT_FOLDER_ID;

 static
 {
  TAG = new TagMetaData("communityselector",
    "Displays a community selector.");

  SELECT_ID = new RequiredTagAttribute("id",
    "The id of the select box.",
    AttributeType.STRING);

  SELECT_NAME = new RequiredTagAttribute("name",
    "The name of the select box.",
    AttributeType.STRING);
 
  ROOT_FOLDER_ID = new RequiredTagAttribute("rootfolderid",
    "The root folder. All communities in this folder and below " +
    "will be displayed.",
    AttributeType.INT);

  SELECT_CLASS = new OptionalTagAttribute("class",
    "The CSS class of the select box.",
    AttributeType.STRING, "objectText");
 }

 public HTMLElement DisplayTag()
 {
  HTMLSelect comms = new HTMLSelect(
    GetTagAttributeAsString(SELECT_NAME),
    GetTagAttributeAsString(SELECT_ID));  

  comms.SetStyleClass(GetTagAttributeAsString(SELECT_CLASS));
  recursiveAddComms(((IPTSession)GetEnvironment().GetUserSession())
   .GetCommunities(), ((IPTSession)GetEnvironment()
   .GetUserSession()).GetAdminCatalog(), comms,
   GetTagAttributeAsInt(ROOT_FOLDER_ID), "");
 
  return comms;
 }

 public ATag Create()
 {
  return new CommunitySelector();
 }

 public TagType GetTagType() {
  return TagType.NO_BODY;
 }
 
 private void recursiveAddComms(IPTObjectManager commObjMgr,
  IPTAdminCatalog adminCatalog, HTMLSelect comms,
  int folderId, String prefix) {
 
  //CAB: add the communities at this level, if any
  IPTQueryResult commsToAdd = commObjMgr.SimpleQuery(folderId,
   PT_PROPIDS.PT_PROPID_NAME);
  for (int i = 0; i < commsToAdd.RowCount(); ++i) {
   comms.AddOption(new HTMLOption(
    Integer.toString(commsToAdd.ItemAsInt(i, PT_PROPIDS.PT_PROPID_OBJECTID)),
    prefix.substring(0, prefix.length() - 3)));
  }
 
  IPTAdminFolder adminFolder = adminCatalog.OpenAdminFolder(folderId, false);
  if (0 == adminFolder.QuerySubfoldersCount()) {
   return; //CAB: base case
  } else {
   IPTQueryResult subFolders = adminFolder.QuerySubfolders(
    PT_PROPIDS.PT_PROPID_OBJECTID + PT_PROPIDS.PT_PROPID_NAME
    + PT_PROPIDS.PT_PROPID_FOLDER_FOLDERTYPE,
    0,
    PT_PROPIDS.PT_PROPID_NAME,
    0,
    -1,
    null);
 
   //CAB: recurse into each subfolder
   for (int i = 0; i < subFolders.RowCount(); ++i) {
    recursiveAddComms(commObjMgr, adminCatalog, comms,
     subFolders.ItemAsInt(i, PT_PROPIDS.PT_PROPID_OBJECTID),
     prefix + subFolders.ItemAsString(i, PT_PROPIDS.PT_PROPID_NAME)
     + " : ");
   }
  }
 }
}

I think the code pretty much speaks for itself, but if you want further explanation, let me know by posting a comment.

Comments

Comments are listed in date ascending order (oldest first)

  • Iiiinteresting. This is very cool, Chris. I might be forced to highjack this and turn it into a pt:data tag πŸ™‚ You have any thoughts on how / where you might approach caching with this? Have you seen the EOD sample tag?

    Posted by: ewwhitley on August 31, 2006 at 9:41 AM

  • Hmmm . . . caching. It’s so fast OOTB that I didn’t think about caching it. πŸ™‚ Plus, I used it on a project where we have fewer than 100 communities, so I didn’t have any problems. I mean, we’re not using the PRC, right? I guess if you wanted to cache it you could doink around with the shared variables in the tag library (session scope), but you’ve got to worry about clearing the cache after a fixed interval of time.

    Posted by: bucchere on August 31, 2006 at 10:31 AM

  • Awesome! Thanks Chris. I just spent about 2 hours last night trying to do almost the exact same thing. This is great!

    Posted by: jturmelle on May 17, 2007 at 7:48 AM

Passing information between Plumtree portlets

This matter has been a subject of a great deal of debate among many of our customers, so I thought I would share my thoughts on the topic. What better place to do it than here on bdg’s Plumtree blog? πŸ˜‰

This post expounds on the many methods you can use to pass information from one portlet to another or, in the more simple case, just store information temporarily for use later by a single portlet. There are at least three approaches I’ve found that accomplish this: the Plumtree Settings approach, the PCC/Adaptive Portlet approach and the backend-system approach. As you’ll see, there are also hybrids of these approaches that may work best for you, depending on your environment. I’m going to describe each of these in detail, but first, allow me back up a bit and explain the problem.

You’re building what Plumtree now calls an IAM (Integrated Activity Management) application. If you don’t think you are, check out this example (requires Flash). If you thought you were building an SOA (Service Oriented Architecture) application, well, a rose by any other name is still a rose, right? Basically, IAM, SOA and Composite Applications all mean the same thing to me, more or less. The way I define them is: you’re building an application that allows your end-users to achieve a business goal, say, an employee enrolling in his or her company’s health benefit plan. In order to pull this off, you need to build integration with several different corporate systems including perhaps getting an employee record from the HR system, deducting pre-tax payments from the employee in the company’s payroll system, and then registering the employee with an external site’s web services-based API to enroll him or her with the third-party benefits provider.

In order to pull this off in Plumtree, you’re going to need to design and build several portlets and perhaps a couple community pages or even multiple communities. Exactly how many portlets and whether to use one page, more than one page or one or more communities is one of the many decisions you’ll need to make in designing your application. All your decisions, BTW, should depend on how you want to structure your navigation and your screens to make your application usable. These decisions should not be ruled by what Plumtree is or isn’t capabile of doing but instead by how you’re going to make it easy (and even fun) for your employee to enroll in health benefits.

So, all that business aside, you’ve designed your application and it uses more than one portlet on, say, different communities. In keeping with our example, let’s say the first portlet allows the employee to confirm or update all his or her personal information and the second portlet asks the employee to pick amounts to be deducted from his or her paycheck for premiums and flexible spending. Now you’ve created at least two technical problems for yourself: 1) how do you get from the Employee Information community to the Payroll Management community and 2) how you get information (or state) from the Personal Information portlet to the Flex Spending portlet?

The answer to the first question is easy – use the pt:openerLink markup tag to create an URL that takes you from the Employee Information community to the Payroll Management community.

The answer to the second question depends on what type of state you need to pass, what form it’s in and how big it is. I’ll keep those things in mind as I discuss the three approaches to passing state.

Plumtree Settings approach

This approach works well if you have small bits of state that can be represented as strings and if you don’t mind having the page refresh (which it’s going to do anyway as you move from one community to another). To pull this off, you’ll need to use a User Setting, a type of setting that is unique for every user but that can be read (and set) by any portlet. First, you’ll need to settle on a name for your User Setting and then configure your Plumtree Web Service to “listen” for that setting. To do this, you need to open your Web Service editor, go to the Preferences page and then under where it says “Add specific preferences that you would like sent to this Web Service,” enter the name of your User Setting. You’ll need to do this in all the Web Services that need access to this information.

In our benefit enrollment application, the most likely thing we’ll need to pass is the employee ID, which shouldn’t be hard to represent as a string. Here’s what the code might look like in the Personal Information portlet:


PorltetContextFactory.createPortletContext(request, response).getResponse().setSettingValue(SettingType.User, "EmployeeID", employeeID);

And here’s the code for your Flex Spending porltet:


String employeeID = PorltetContextFactory.createPortletContext(request, response).getRequest().getSettingValue(SettingType.User, "EmployeeID");

PCC/Adaptive approach

The Plumtree Settings approach works well in this case, but what if you wanted to do this without a page refresh? Then I might suggest looking at two different adaptive portlet patterns: the Master-Detail pattern and the Broadcast-Listener pattern. These patterns do exactly the same thing, but with one major difference. The Master-Detail pattern assumes that you have no more than two portlets and that your two portlets are on the same page and the Broadcast-Listener allows you to send information to portlets on different pages and to broadcast from one portlet to many listener portlets.

Here’s how the PCC/Adaptive approach fits into our example: when the employee finishes updating his or her personal information in the Personal Information portlet, he or she is expected to click a button or link to indicate completion. The button or link needs to be tied to a javascript method that passes the employee’s ID to a PTHTTPGETRequest for the content (or a segment of the content) of the Flex Spending portlet using a querystring argument. When setting up a PTHTTPGETRequest, you’ll notice that the second argument is essentially a function pointer that allows this object to execute a callback when the response completes. All that callback function needs to do is place the content of the Flex Spending portlet into a div tag and you’re done.

I think Plumtree does a great job with all their Adaptive Portlet examples – you can literally copy and paste them into your portlet code and they just work. However, if you get stuck and need more help, feel free to post here or on portal.plumtree.com.

Backend System approach

So we’ve talked about how to pass a small piece of state from one portlet to another, covering the with-page-refresh and without-page-refresh cases in addition to the single-community/page and multiple-community/page case. But what about the case where your state is not quite as simple as a employee ID?

The first question I have to ask is, why are you trying to pass a lot of state? Isn’t the employee ID sufficient to go into the backend system and get whatever data you need? If the answer is no, then what form is your state in? Do you have a piece of text or an object? If you have a small piece of text, you can use either of the first two approaches: set it as a User Setting or URL-encode it and pass it around using Master-Detail or Broadcast-Listener. If you have a small object, you can serialize it and then Base-64 or UUEncode it and then use one of the first two approaches. But you need to be very careful. There are limits to the amount of data you can put on a querystring and limits to amount of data you can put in a header. These limits are not enforced by the HTTP spec, but the spec warns you about artificial limits on URL length based on web server implementations. Mark Nottingham, Senior Principal Technologist at BEA Systems, cautions against using headers larger than 2048 bytes in this post. Remember, all Plumtree Settings are passed back and forth between the portal and portlets as HTTP headers.

So you’re stuck with a somewhat bulky object, let’s say in our example, the entire employee’s record in a DTO, and so you’ve ruled out the first two approaches. Now what?

If you’re a traditional web programmer who’s new to Plumtree, you might have been thinking this all along – what about the HTTP session? Isn’t storing and passing state from page to page what sessions are for?

Sessions are great place for things like an employee record DTO, but in Plumtree, each portlet has its own session on the remote server. This means that you can put whatever you want in the session, but you’re not going to be able to share it with other portlets. Using the session also has interesting ramifications if you’re using Plumtree caching, but that’s the subject of another post.

So, what about the HTTP application? If you have a single remote server and all your portlets are using it, you can store objects, like the employee record DTO in the application, using a key consisting of the employee ID as follows (inside a Java servlet):


synchronized(this) {
this.getServletContext().setAttribute("com.bdgportals.iam.example.EmployeeRecordDTO" + employeeID, employeeRecordDTO);
}

Different portlets can now access this attribute, but you need to make sure that all your code that writes to this attribute is threadsafe.

Of course, this assumes that you’re using the same application server for all your portlets, which is not necessarily true, even for a single application (because you may be load-balancing your porltets across different application servers).

* * *

If there are other ways you’ve found to pass state between portlets, I’d love to hear about them via comments on this post.

Query filters explained, at last

I can’t even begin to count the number of times I’ve been asked how to write a query filter in Plumtree. Yesterday, I had to write one for myself and lo and behold, I was confused. I’ve come to the conclusion that they’re just hard to write. Despite this, they’re incredibly useful when trying to lookup objects in the Plumtree API.

(Note: this post does not apply to writing portlets or other integrations. It’s only useful when writing Pluggable Navigation, PEIs, or other customizations to the UI.)

To use a query filter, you’ll first need access to a Plumtree user’s session. Let’s assume that you have one in the variable plumtreeSession and that it’s already connected as a user who has at least read-level access to some objects. This query will work for any object, but just for the sake of example, let’s use Groups. Also, I wrote this in Java, but it works almost exactly the same way in C#.

First you need to get an IPTObjectManager as shown here:

IPTObjectManager groups = plumtreeSession.GetUserGroups();

Next, we’ll build up the query filter, which is always an Object[][].

Object[][] filter = new Object[3][1];
filter[0][0] = new Integer(PT_PROPIDS.PT_PROPID_NAME);
filter[1][0] = new Integer(PT_FILTEROPS.PT_FILTEROP_EQ_NOCASE);
filter[2][0] = "Everyone";

Let’s go through that line by line. First of all, why did I choose 3,1 for my array dimensions? All query filters are 2D arrays. The first dimension always has three items:

    1. The name of the property on which you’re filtering
    2. The operator (equals, less than, greater than, etc.)
    3. The value of the property you’re evaluating

Since it’s an Object[][], we can’t use primatives, so we must box our constants in Integer objects because the constants themselves are primatives.

When using the Query method that supports a query filter, you also need to specify an ordering attribute. You can only order ascending, but you can order by up to three properties which will be applied in the order they are put in the array, as shown below:

int[] orderBy = {PT_PROPIDS.PT_PROPID_NAME, PT_PROPIDS.PT_PROPID_OBJECTID};

In the code above, I’m asking to order first by name, then by object ID. (Since no two names can be the same in Plumtree, this dual ordering doesn’t really make much sense, but oh well.)

Now, finally, it’s time to run the query:

IPTQueryResult group = groups.Query(PT_PROPIDS.PT_PROPID_OBJECTID + PT_PROPIDS.PT_PROPID_NAME, -1, orderBy, 0, -1, filter);

Let’s break down those parameters one by one. The first, PT_PROPIDS.PT_PROPID_OBJECTID + PT_PROPIDS.PT_PROPID_NAME is a bitmask of properties you want to include in your results.

The second, -1, means query all folders in the admin directory. (If you want to restrict your query to a single folder (and all folders below it), use the folder id here. If you want to query down two distinct folder trees, you’ll need to run two queries.

The third, orderBy is your ordering attribute(s), explained above.

The fourth, 0 is your starting row.

The fifth, -1 is the number of rows to return (-1 means all rows). This parameter, along with the starting row, can be very useful for pagination.

The sixth and final parameter is your beloved query filter.

We did it! Now that wasn’t so bad, eh? Iterating through the results set is trivial, so I won’t cover it here, but if you have you have a question, feel free to make a comment here or post on the developer forums.