function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
jhartjhart 

PageReference question - how to move in & out of package subdomains?

Let's say I have an unpackaged page that wants to point at a packaged page via a standard <a> tag:

Code:
<a href="/apex/i__packagedPage">Go from unpackaged to packaged</a>

 
That works fine, as "https://na1.salesforce.com/apex/i__packagedPage" redirects to "https://i.na1.visual.force.com/apex/packagedPage".


However, I cannot do the reverse.  If I'm on a packaged page, this:

Code:
<a href="/apex/unpackagedPage">Go from packaged to unpackaged</a>

 
Does not work, because "https://i.na1.visual.force.com/apex/unpackagedPage" does not redirect back to the unpackaged domain; instead the sub-domain host just throws an error.


An understandable workaround would be if you had to use a controller method get the right URL by using a PageReference:

Code:
// packaged page:
<a href="{!unpackagedUrl}">Go from packaged to unpackaged</a>

// packaged controller:
public string getUnpackagedUrl() {
  PageReference p = new PageReference('/apex/unpackagedPage');
  return p.getUrl();
  }


But that doesn't work either - the code above generates the same relative URL, just like our HREF example above (ie, PageReference doesn't have any additional domain smarts).


So whatever is generating these URLs from the https://i.na1.visual.force.com" domain has to know that "https://na1.salesforce.com" is the appropriate unpackaged domain for the current user.

On the client side, this could be done via javascript.


But - I need to do this on the *server* side, because the actual use case is redirecting from a packaged page to an unpackaged version of that page (in order to support client customizations of our packaged UI):

Code:
// page:
<apex:page controller="PackagedController" action="{!checkForUnpackagedOverride}">


// controller:
public PageReference checkForUnpackagedOverride() {
  ApexPage[] override = [select Name from ApexPage where ...];
  if (p.size() == 0) return null;
PageReference p = new PageReference('/apex/' + override[0].Name); p.setRedirect(true); return p; }

 
But this doesn't work.  What can I do on the server side to figure out my salesforce instance URL?

I'll post what I find, if anything.
jwetzlerjwetzler
In a page:
{!$Page.myPageName}

In a controller:
PageReference p = Page.myPageName;
return p;

Edit: I should mention that with a namespace the syntax is Page.namespace__myPageName.


Message Edited by jwetzler on 12-15-2008 03:04 PM
jhartjhart
Jill,

The name of the unpackaged page isn't known at compile time - the whole point is that customers *can* override our packaged pages with custom versions, but they don't have to.  We use a naming convention to find a customized, unpackaged version of the current packaged page; but that page may not even exist, so we can't use the method you propose.


jwetzlerjwetzler
I see.  This is the first I've heard of anyone needing to do something like this and I don't think there's a good solution right now.  What kind of solution would you propose?  It sounds like even if you could achieve this, your namespaced package is still reliant on someone either installing your unmanaged package as well, or creating their own page with the same naming convention.

I recall seeing an idea on the ideaExchange for a method in apex that would give you the instance that the user was on.  Can't seem to track it down right now but you might want to look for that and promote it.  Although really just being able to look up the name of the instance seems like a hack at best.  I'm wondering if there's a more elegant solution involving components somewhere in there.
jhartjhart
Hi Jill,

Here's the use case for us:

Our application logs emails to Salesforce.com, and it provides a "Pending Addresses" page that captures all your email traffic that *doesn't* match an existing Contact/Lead in Salesforce.  This page shows you each such address, along with a list of possible actions for that address (eg, create Contact / create Lead / ignore this address - see image below).

Our customers have custom required fields on Lead & Contact objects, and so need to display input fields for those required fields on our page.  There is currently no way to do this elegantly.  Ideally our controller would simply scan their object model & auto-generate input fields for required object fields, but this would require programmatic Visualforce component tree manipulation, which doesn't exist.

The only way we've come up with to handle this requirement is for customers to be able to run their own version of our page.  This custom page must be unpackaged (rolling new packaged versions for each customer is the path to insanity).  Our customers are savvy enough to create their own unpackaged pages (via the UI) if we provide them with the markup, which is easy for us to generate.  The unpackaged page uses the exact same controller as the packaged page (all its properties etc. are global).

Thus, the requirement.

In terms of solutions, a few ideas come to mind:

a)  Redirects. 

Just like the unpackaged domain will redirect to the correct packaged domain (https://na1.salesforce.com/apex/i__myPage redirects to https://i.na1.visual.force.com/apex/myPage), have the packaged domain do the same thing in *reverse* when asked for a page that doesn't exist in the packaged domain but does exist unpackaged (so https://i.na1.visual.force.com/apex/otherPage redirecs to https://na1.salesforce.com/apex/otherPage if "otherPage" does not exist in the package but does exist unpackaged).

This is somewhat hacky because the URL is kind of ambiguous (requires figuring out if the page exists in packaged/unpackaged versions), but at least it lets packaged pages pretend they are in the same URL space as unpackaged pages.


b) Provide a global "hostname" field that can be accessed both from within Apex code and as a merge field from within an Apex page ... just like the {!$Site.Domain} merge field works for Force.com sites.

This is probably the better solution; while it requires developers to be aware of the namespace-vs-non-namespace hostname distinction this isn't necessarily a bad thing.  I don't necessarily agree with your comment about this global being a hack - the separate-domains-for-packaged-apps decision is what it is, and we developers need tools to deal with this reality.


If you can't find the idea on the IdeaExchange, I doubt I will be able to either.


"Pending" page screenshot:



jhartjhart
The funny thing is that this information is actually pretty easily available to the page via the $Api.Partner_Server_URL_* merge codes.

But that class isn't accessible (nor documented) from within Apex Code.

I think I have two options.  Both are suboptimable, but one is good enough.

a)  Divine the server URL from local object IDs.

The fourth character of an object ID is the instance ID, so we could build & maintain our own hardcoded mapping from this character to its server URL.

This is a bad idea on many levels.


b) Use an "formula" object field to make the server URL available via SOQL.

We already have a singleton object that stores our app's global config (AAConfig).

We'll add a new field to this object called "BaseUrl" and define its formula as:

Code:
LEFT($Api.Partner_Server_URL_140, FIND(".com/",$Api.Partner_Server_URL_140)+3)

 
Then we can just query this field to get, for example, "https://na5.salesforce.com"

While this is also pretty disgusting, it is relatively future-proof and will last us until Salesforce provides a better method.




jhartjhart
Note - it turns out that formula returns different values depending on context.

Calling it from "executeAnonymous" in my DE org gets me:

https://na5-api.salesforce.com

whereas pulling it out in a packaged Visualforce page returns:

https://i.na5.visual.force.com

So the Apex code that queries this value will have to munge it further to get the actual "unpackaged" hostname.
TehNrdTehNrd
Here is the idea Jill mentioned:

http://ideas.salesforce.com/article/show/10091004/Apex_method_to_identify_salesforcecom_server#skin=le

And here is a thread discussing this problem:

http://community.salesforce.com/sforce/board/message?board.id=apex&thread.id=6132

If you are using a controller that is working with a page this appeared to be the best option to get the server host/subdomain:

string host = ApexPages.CurrentPage().getHeaders().get('Host');






Message Edited by TehNrd on 12-16-2008 09:19 AM
jhartjhart
Thanks, TehNrd!

I actually already uploaded a managed packaged with my method, so it's there to stay =)

It looks like your method is more elegant, but only works in Visualforce pages.

My method is less elegant, but works in all contexts (including webservice calls and triggers):

Here's the full implementation of my method:

The custom object field is defined like so:

Code:
LEFT($Api.Partner_Server_URL_140, FIND(".com/",$Api.Partner_Server_URL_140)+3)

It always returns non-null, but what it returns depends on context, so here's the Apex code that we have (so far) to translate those back into "normalized" server URLs:

Code:
// returns https://na5.salesforce.com or https://na1.salesforce.com (etc)
// NB: AAConfig__c.BaseURL__c value depends on calling context:
// https://na5.salesforce.com      (desired)
// https://na5-api.salesforce.com (executeAnonymous + standard API) // https://i.na5.visual.force.com (packaged controllers, packaged triggers)
public static string instanceUrl() { string ret = AAConfig.getConfig().BaseURL__c; ret = ret.replace('-api.','.'); if (ret.contains('visual.force')) ret = ret.replace('://i.', '://').replace('visual.force','salesforce'); return ret; }

It's definitely down-and-dirty (it might be smarter to use regexp's, though I'm not convinced), and the "visual.force" replacement line hard-codes our package namespace ("i"), so YMMV.

I also don't guarantee that it covers all possible return values - there might be contexts in which it gets the transform wrong.  For now I've tested it in the contexts listed in the code.


jhartjhart
Note the above transform is incomplete, as salesforce's older pods (na0,ap0,eu0) use non-standard hostnames for the UI hostnames (ssl,ap, and emea, respectively).

Note this issue will also strike the ApexPages.CurrentPage().getHeaders().get('Host') method if you're trying to derive the main UI hostname from a packaged controller.

Here's a newer version of the transform code:

Code:
// returns the base UI url (https://na5.salesforce.com, https://na1.salesforce.com, https://emea.salesforce.com, etc)
// Note that AAConfig__c.BaseURL__c changes depending on calling context:
// eg:
// https://na5.salesforce.com      (desired)
// https://na5-api.salesforce.com  (executeAnonymous, standard SOAP API)
// https://i.na5.visual.force.com  (packaged controllers, packaged triggers)
// 
// Note that salesforce's naming convention isn't consistent, as the instance moniker (na1, na5 etc)
// changes between the API & UI hostnames for certain older pods
// see also http://salesforce.phollaio.com/2007/01/11/salesforces_url_structure/)
// 
// API HOST               => UI HOST             
// ----------------------    --------------------
// eu0-api.salesforce.com => emea.salesforce.com
// na0-api.salesforce.com => ssl.salesforce.com
// ap0-api.salesforce.com => ap.salesforce.com
//
public static string instanceUrl() {
  string MYNAMESPACE = 'i';
  string ret = AAConfig.getConfig().BaseURL__c;
  ret = ret.replace('-api.','.');     // context: executeAnon, standard SOAP API
  if (ret.contains('visual.force'))   // context: packaged code
    ret = ret.replace('://' + MYNAMESPACE + '.', '://').replace('visual.force','salesforce');
  // handle older pod naming inconsistency:
  ret = ret.replace('://eu0.','://emea.');
  ret = ret.replace('://na0.','://ssl.');
  ret = ret.replace('://ap0.','://ap.');
  return ret;
  }

 

jhartjhart

New info!

 

The "c" namespace is now used by Salesforce for unpackaged pages, so regardless of what context/host you are in (packaged page, unpackaged page, or salesforce standard page), this URL:

 

 

<a href="/apex/c__page">go to unpackaged page</a>

 

Will automatically redirect to https://c.na4.visual.force.com/apex/page

 

Which is exactly what I need for my purposes.