How to embed language in Sitecore-uCommerce nice URLs? - sitecore

I am using the default configuration of uCommerce and see that uCommerce nice URLs are not language aware: http://sitename/catalogname/productname/c-XX/p-YY.
What should I do to have language in those URLs like this: http://sitename/en/catalogname/productname/c-XX/p-YY ?
Here is the configuration:
<linkManager defaultProvider="sitecore">
<providers>
<clear />
<add name="sitecore" type="Sitecore.Links.LinkProvider, Sitecore.Kernel" addAspxExtension="false" alwaysIncludeServerUrl="false" encodeNames="true" languageEmbedding="always" languageLocation="filePath" lowercaseUrls="true" shortenUrls="true" useDisplayName="true" />
</providers>
</linkManager>
Here is how I use it:
public WebshopProduct Map(UCommerceProduct uProduct)
{
ProductCatalog catalog = CatalogLibrary.GetCatalog(25);
IUrlService urlService = ObjectFactory.Instance.Resolve<IUrlService>();
...
var url = urlService.GetUrl(catalog, uProduct) // this returns "/catalogname/productname/c-XX/p-YY"
//And I would like to have "/en/catalogname/productname/c-XX/p-YY"
}

Adding language to URL depends on how you are rendered links. If you don't pass specific parameters than Sitecore (and uCommerce as part of Sitecore) uses LinkManager configuration sitecore>linkManager>providers: languageEmbedding adn languageLocation attributes. You should have languageEmbedding="always" and languageLocation="filePath"
P.S.
But, be aware if you using their demo or something based on their demo(e.g. from certification courses): they uses regular ASP.Net MVC(not Sitecore MVC). And links are not rendered via LinkManager and you should put language to URL by youself. Register routed with language code that is embedded to them.

Here is what I have come up with:
public static class TemplateIDs
{
// sitecore/ucommerce item's template id
public static ID UCommerce => new ID("{AABC1CFA-9CDB-4AE5-8257-799D84A8EE23}");
}
public static class ItemExtensions
{
public static bool IsUCommerceItem(this Item item)
{
var items = item.Axes.GetAncestors();
return items.Any(x => x.TemplateID.Equals(TemplateIDs.UCommerce));
}
}
public static string GetItemUrlByLanguage(Sitecore.Globalization.Language language)
{
if (Context.Item.IsUCommerceItem() && SiteContext.Current.CatalogContext.CurrentProduct != null && SiteContext.Current.CatalogContext.CurrentProduct.Guid == Context.Item.ID.Guid)
{
ProductCatalog catalog = CatalogLibrary.GetCatalog(25);
IUrlService urlService = ObjectFactory.Instance.Resolve<IUrlService>();
var url = "/" + language.CultureInfo.TwoLetterISOLanguageName + urlService.GetUrl(catalog, SiteContext.Current.CatalogContext.CurrentProduct);
return url;
}
else
{
//Normal URL creation
using (new LanguageSwitcher(language))
{
var options = new UrlOptions
{
AlwaysIncludeServerUrl = true,
LanguageEmbedding = LanguageEmbedding.Always,
LowercaseUrls = true
};
var url = LinkManager.GetItemUrl(Context.Item, options);
url = StringUtil.EnsurePostfix('/', url).ToLower();
return url;
}
}
}

Related

Custom Provider for AWS SSM using Microsoft.Configuration.ConfigurationBuilders

I seem to be stuck at developing a custom Key/Value pair provider for Amazon's System Manager Parameter Store (SSM) using NETFramework 4.7.1 that utilizes Microsoft.Configuration.ConfigurationBuilders.
The implementation:
using System;
using System.Collections.Generic;
using Amazon.SimpleSystemsManagement;
using Amazon.SimpleSystemsManagement.Model;
using Microsoft.Configuration.ConfigurationBuilders;
using System.Linq;
using System.Diagnostics;
using System.Collections.Specialized;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using System.Configuration;
using System.Threading.Tasks;
namespace AXS.Configurations
{
public class ParameterStoreConfigBuilder : KeyValueConfigBuilder
{
public const string envTag = "Environment";
public const string appNameTag = "AppName";
private IAmazonSimpleSystemsManagement client;
/// <summary>
/// Gets or sets an environment (dev|qa|staging|production)
/// </summary>
public string Environment { get; set; }
/// <summary>
/// Gets or sets a AppName
/// </summary>
public string AppName { get; set; }
public ParameterStoreConfigBuilder(IAmazonSimpleSystemsManagement client,
string appName,
string environment)
{
this.client = client;
Environment = environment.ToLower();
AppName = appName;
}
public ParameterStoreConfigBuilder()
{
client = new AmazonSimpleSystemsManagementClient();
}
public override string Description => "Parameter Store";
public override string Name => "SSM";
protected override void LazyInitialize(string name, NameValueCollection config)
{
Optional = false;
base.LazyInitialize(name, config);
string env = UpdateConfigSettingWithAppSettings(envTag);
if (string.IsNullOrWhiteSpace(env))
throw new ArgumentException($"environment must be specified with the '{envTag}' attribute.");
Environment = env;
string appName = UpdateConfigSettingWithAppSettings(appNameTag);
if (string.IsNullOrWhiteSpace(appName))
throw new ArgumentException($"appName must be specified with the '{appNameTag}' attribute.");
AppName = appName;
client = new AmazonSimpleSystemsManagementClient("","", Amazon.RegionEndpoint.USWest2);
}
public override ICollection<KeyValuePair<string, string>> GetAllValues(string prefix)
{
Trace.TraceInformation($"return values prefix {prefix}");
if (client == null)
return null;
var parameters = new List<Parameter>();
string nextToken = null;
do
{
var response = client.GetParametersByPath(new GetParametersByPathRequest { Path = prefix, Recursive = true, WithDecryption = true, NextToken = nextToken });
nextToken = response.NextToken;
parameters.AddRange(response.Parameters);
} while (!string.IsNullOrEmpty(nextToken));
return parameters.Select(p => new
{
Key = p.Name,
p.Value
}).ToDictionary(parameter => parameter.Key, parameter => parameter.Value, StringComparer.OrdinalIgnoreCase);
}
public override string GetValue(string key)
{
return Task.Run(async () => { return await GetValueAsync(key); }).Result;
}
private async Task<string> GetValueAsync(string key)
{
var name = $"/{Environment}/{AppName}/{key.Replace(':', '/')}";
Trace.WriteLine($"get value async:{name}");
if (client == null)
return null;
try
{
Trace.TraceInformation($"fetch key {name}");
var request = new GetParameterRequest
{
Name = name,
WithDecryption = true
};
var response = await client.GetParameterAsync(request);
var parameter = response.Parameter;
var value = parameter.Type == ParameterType.SecureString ? "*****" : parameter.Value;
Trace.TraceInformation($"fetched name={name} value={value}");
return value;
}
catch (Exception e) when (Optional && ((e.InnerException is System.Net.Http.HttpRequestException) || (e.InnerException is UnauthorizedAccessException))) { }
return null;
}
}
}
The problem seems to be that AWS SSM client never gets created.
If I change the code and try to instantiate in the constructor I get a stack overflow exception due to recursion.
Any ideas on how to force to get AmazonSimpleSystemsManagementClient created?
The code uses guidance from https://github.com/aspnet/MicrosoftConfigurationBuilders
The App.Config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection,
System.Configuration, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a"
restartOnExternalChanges="false"
requirePermission="true" />
</configSections>
<configBuilders>
<builders>
<add name="ParameterStore" Environment="development" AppName="myAppNameforParmStore" type="AXS.Configurations.ParameterStoreConfigBuilder, AXS.Configurations" />
<add name="Env" prefix="appsettings_" stripPrefix="true" type="Microsoft.Configuration.ConfigurationBuilders.EnvironmentConfigBuilder, Microsoft.Configuration.ConfigurationBuilders.Environment, Version=2.0.0.0, Culture=neutral" />
</builders>
</configBuilders>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.1" />
</startup>
<appSettings configBuilders="Env,ParameterStore">
<add key="Url" value="URL Value for from paramter Store" />
<add key="Secret" value="Some Secret value decrypted" />
</appSettings>
</configuration>
Thanks
UPDATE
I posted an updated version of the AwsSsmConfigurationBuilder, and a sample ASP.NET Web Forms project that uses it, on my GitHub:
https://github.com/Kirkaiya/AwsSsmConfigBuilderPoC/
Disclaimer: This is a proof-of-concept (POC) for a custom ConfigurationBuilder for ASP.NET 4.7.1 or higher (running on .NET Framework obviously). It's a POC, so it doesn't do anything besides allow you store Configuration AppSettings in AWS Parameter Store (a feature of Simple Systems Manager). So, clearly, don't use this in production without productizing and testing it!
Prerequisites:
Your project must target .NET Framework 4.7.1 or higher
Include NuGet package Microsoft.Configuration.ConfigurationBuilders.Base
Have parameters in AWS SSM Parameter Store that have the same name (not counting the prefix) as parameters in your web.config file, and vice-versa.
Notes
In order to avoid recursively calling a concrete constructor or Initialize, I used a static constructor to instantiate the AmazonSimpleSystemsManagementClient, which is held in a static member.
Web.Config additions
Note: change the assembly/class-name of your builder to match yours, etc.
<configSections>
<section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false" />
</configSections>
<configBuilders>
<builders>
<add name="ParameterStore" ssmPrefix="/padnugapp/ApiKeys" type="Microsoft.Configuration.ConfigurationBuilders.AwsSsmConfigBuilder, AspNetWebFormsSample" />
</builders>
</configBuilders>
<appSettings configBuilders="ParameterStore">
<add key="TestKey" value="TestKey Value from web.config" />
<add key="TwitterKey" value="TwitterKey value from web.config" />
</appSettings>
And the AwsSsmConfigBuilder.cs file:
namespace Microsoft.Configuration.ConfigurationBuilders
{
public class AwsSsmConfigBuilder : KeyValueConfigBuilder
{
private string BaseParameterPath = "/padnugapp/ApiKeys";
private static IAmazonSimpleSystemsManagement _client;
static AwsSsmConfigBuilder()
{
_client = new AmazonSimpleSystemsManagementClient();
}
public override void Initialize(string name, NameValueCollection config)
{
base.Initialize(name, config);
if (config["ssmPrefix"] == null)
return;
BaseParameterPath = config["ssmPrefix"];
}
public override ICollection<KeyValuePair<string, string>> GetAllValues(string prefix)
{
if (_client == null)
return null;
var request = new GetParametersByPathRequest
{
Path = $"{BaseParameterPath}/{prefix}",
WithDecryption = true,
};
var response = _client.GetParametersByPathAsync(request).Result;
var result = response.Parameters.ToDictionary(param => param.Name, param => param.Value, StringComparer.OrdinalIgnoreCase);
return result;
}
public override string GetValue(string key)
{
if (_client == null)
return null;
var request = new GetParameterRequest
{
Name = $"{BaseParameterPath}/{key}",
WithDecryption = true,
};
var response = _client.GetParameterAsync(request).Result;
return response.Parameter.Value;
}
}
}
The code I put into a web-forms (.aspx) page that renders the two appSettings items in HTML:
TestKey =
<%=(System.Configuration.ConfigurationManager.AppSettings["TestKey"]) %>
<br />
TwitterKey =
<%=(System.Configuration.ConfigurationManager.AppSettings["TwitterKey"]) %>
I can't stress enough that this is just for a demo I'm doing, and not tested in any way, shape or form except on my laptop ;-)

Get Rendered HTML From Sitecore Item

I need to get the rendered HTML output from a given Sitecore item, assuming it has a layout. I need it to be the latest version of the rendered content whether it's published or not. Using a web request approach like WebClient or HtmlAgility pack will not work because they make the request as an anonymous user which will only render the latest published version (and I need the latest version no matter the state.) Any thoughts? I have everything working I just cant find a way to impersonate or elevate the rights while I execute the page requests.
You could go the WebClient or HtmlAgility pack, but silently login the user based on a token in the query string:
public static class UserExtensions
{
public const string TokenKey = "UserToken";
public const string TokenDateKey = "UserTokenDate";
public static ID CreateUserToken(this User user)
{
if (user.IsAuthenticated)
{
var token = ID.NewID;
user.Profile.SetCustomProperty(TokenKey, token.ToString());
user.Profile.SetCustomProperty(TokenDateKey, DateTime.Now.ToString());
user.Profile.Save();
return token;
}
else
return ID.Null;
}
public static bool IsTokenValid(this User user, string token, TimeSpan maxAge)
{
var tokenId = ID.Null;
if (ID.TryParse(token, out tokenId))
{
var minDate = DateTime.Now.Add(-maxAge);
var tokenDateString = user.Profile.GetCustomProperty(TokenDateKey);
var tokenDate = DateTime.MinValue;
DateTime.TryParse(tokenDateString, out tokenDate);
if (tokenDate < minDate)
return false;
var storedToken = user.Profile.GetCustomProperty(TokenKey);
var storedTokenId = ID.NewID;
if (ID.TryParse(storedToken, out storedTokenId))
return storedTokenId == tokenId;
}
return false;
}
}
Then patch in a HttpRequestProcessor to look for the token:
public class SilentUserLogin : HttpRequestProcessor
{
public TimeSpan MaximumAge
{
get;
set;
}
public override void Process(HttpRequestArgs args)
{
var userValue = args.Context.Request.QueryString["user"];
var tokenValue = args.Context.Request.QueryString["token"];
if (!string.IsNullOrEmpty(userValue) && !string.IsNullOrEmpty(tokenValue))
{
// find user
var user = User.FromName(userValue, AccountType.User);
if (user != null)
{
// Check token is valid
if ((user as User).IsTokenValid(tokenValue, MaximumAge))
{
// log user in
AuthenticationManager.Login(user as User);
}
else
Log.Audit("User token has expired for user: '{0}'".FormatWith(user.Name), this);
}
else
Log.Audit("Failed to locate auto login user " + userValue, this);
}
}
Patch this in with a config file:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<httpRequestBegin>
<processor type="Namespace.SilentUserLogin,Assembly" patch:after="*[#type='Sitecore.Pipelines.HttpRequest.StartMeasurements, Sitecore.Kernel']">
<MaximumAge>00:02:00</MaximumAge>
</processor>
</httpRequestBegin>
</pipelines>
</sitecore>
</configuration>
Finally, call the page via WebClient or HtmlAgility:
var token = Sitecore.Context.User.CreateUserToken();
var url = new UrlString();
url.HostName = HttpContext.Current.Request.Url.Host;
url.Protocol = HttpContext.Current.Request.IsSecureConnection ? "https" : "http";
url.Path = "/";
url["sc_itemid"] = myItem.ID.ToString();
url["sc_lang"] = myItem.Language.ToString();
// Add parameters to allow accessing the master DB
url["user"] = Sitecore.Context.User.Name;
url["token"] = token.ToString();
// Call the url here
This code was cribbed from a similar situation where I needed a URL to feed to a PDF generation library, which behind the scenes fired up IE and hit the site as an anonymous user. This way we could pass a limited time security token via the query string.
You can setup a "preview" site that shows content from the master database as opposed to the public-facing published content. This article will help setting that up: How to Setup a Sitecore Preview Site to Review Content Before Publishing
Once you have this setup on a unique URL, you can then make a WebRequest to pages or use HtmlAgilityPack.

Power user access to Sitecore Recycle Bin

Does anyone know of a solution to allow users in a certain role to view all items in the Sitecore Recycle Bin?
Currently, only admins can see all deleted items. Users can only see items they have deleted.
There isn't a way out of the box, the SqlArchive.GetEntries checks against user.IsAdministrator to show all entries in the archive.
You would need to implement a custom Archive provider and override the GetEntries method to work from a role.
Example:
public class CustomSqlArchive : SqlArchive
{
public CustomSqlArchive(string name, Database database)
: base(name, database)
{
}
protected override IEnumerable<ArchiveEntry> GetEntries(User user, int pageIndex, int pageSize, ID archivalId)
{
Assert.IsNotNull(archivalId, "archivalId");
var arrayList = new ArrayList(new[] { "archiveName", this.Name });
var str1 = "SELECT * FROM \r\n (SELECT {0}Archive{1}.{0}ArchivalId{1}, {0}Archive{1}.{0}ItemId{1}, {0}ParentId{1}, {0}Name{1}, {0}OriginalLocation{1}, \r\n {0}ArchiveDate{1}, {0}ArchivedBy{1}, ROW_NUMBER() OVER(ORDER BY {0}ArchiveDate{1} DESC, {0}ArchivalId{1}) as {0}RowNumber{1}\r\n FROM {0}Archive{1} \r\n WHERE {0}ArchiveName{1} = {2}archiveName{3}";
var showAllItems = user.IsInRole("Super User Role") || user.IsAdministrator;
if (user != null && !showAllItems)
{
str1 = str1 + " AND {0}ArchivalId{1} IN (SELECT {0}ArchivalId{1}\r\n FROM {0}ArchivedVersions{1} WHERE {0}ArchivedBy{1} = {2}archivedBy{3}) ";
arrayList.AddRange(new[] { "archivedBy", user.Name });
}
if (archivalId != ID.Null)
{
str1 = str1 + " AND {0}ArchivalId{1} = {2}archivalId{3}";
arrayList.Add("archivalId");
arrayList.Add(archivalId);
}
var str2 = str1 + ") {0}ArchiveWithRowNumbers{1}";
if (pageSize != int.MaxValue)
{
str2 = str2 + " WHERE {0}RowNumber{1} BETWEEN {2}firstRow{3} AND {2}lastRow{3}";
var num1 = (pageIndex * pageSize) + 1;
int num2 = pageSize == int.MaxValue ? int.MaxValue : (pageIndex + 1) * pageSize;
arrayList.AddRange(new[] { "firstRow", num1.ToString(), "lastRow", num2.ToString() });
}
return this.GetEntries(str2 + " ORDER BY {0}ArchiveDate{1} DESC, {0}ArchivalId{1}", arrayList.ToArray());
}
}
You would then need to add your custom provider to the config:
<archives defaultProvider="custom" enabled="true">
<providers>
<clear />
<add name="custom" type="Sitecore.Data.Archiving.SqlArchiveProvider, Sitecore.Kernel" database="*" />
<add name="sql" type="Sitecore.Data.Archiving.SqlArchiveProvider, Sitecore.Kernel" database="*" />
<add name="switcher" type="Sitecore.Data.Archiving.SwitchingArchiveProvider, Sitecore.Kernel" />
</providers>
</archives>
Then add a role called Super User Role and put any users you want to have that access as members.
** note - code is untested **
Below is a similar approach to Richard's answer, but instead of copying all of the logic within GetEntries(), it spoofs the admin user. You will also need to implement a SqlArchiveProvider in addition to the CustomSqlArchive itself.
SQL Archive
public class CustomSqlArchive : SqlArchive
{
private const string PowerUserRole = #"sitecore\Power User";
private const string AdminUser = #"sitecore\Admin";
public AvidSqlArchive(string name, Database database) : base(name, database) { }
protected override IEnumerable<ArchiveEntry> GetEntries(User user, int pageIndex, int pageSize, ID archivalId)
{
if (user != null && Role.Exists(PowerUserRole) && user.IsInRole(PowerUserRole))
{
User admin = User.FromName(AdminUser, true);
return base.GetEntries(admin, pageIndex, pageSize, archivalId);
}
return base.GetEntries(user, pageIndex, pageSize, archivalId);
}
}
SQL Archive Provider
public class CustomSqlArchiveProvider : SqlArchiveProvider
{
protected override Sitecore.Data.Archiving.Archive GetArchive(XmlNode configNode, Database database)
{
string attribute = XmlUtil.GetAttribute("name", configNode);
return !string.IsNullOrEmpty(attribute) ?
new CustomSqlArchive(attribute, database) :
null;
}
}
Configuration Patch
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<!-- Use custom archive that allows users in the "Power User" role to see other user's items by spoofing the admin user -->
<archives defaultProvider="sql" enabled="true">
<patch:attribute name="defaultProvider">custom</patch:attribute>
<providers>
<clear />
<add name="custom" type="Example.CustomSqlArchiveProvider, Example" database="*" />
<add name="sql" type="Sitecore.Data.Archiving.SqlArchiveProvider, Sitecore.Kernel" database="*" />
<add name="switcher" type="Sitecore.Data.Archiving.SwitchingArchiveProvider, Sitecore.Kernel" />
</providers>
</archives>
</sitecore>
</configuration>

Using path parameters in <view-id> with PrettyFaces

Many pages in a typical JSF applications are dynamic, meaning that there is a template view that would be used to render every object of a given type. For these pages PrettyFaces rewriting solution works great and effortless. An example is a web application that displays a product, basing on its id, or other unique field. There is typically one view related to such a display, say product.xhtml, and one view parameter, holding the unique field of a product, say name.
With a simple setting we get all requests like /product.xhtml?name=exact-product-name rewritten as, for example, /products/exact-product-name:
The URL mapping:
<url-mapping id="viewProduct">
<pattern value="/products/#{ name : productBean.name }" />
<view-id value="/store/product.xhtml" />
<action> #{ productBean.loadData } </action>
</url-mapping>
The view:
<f:metadata>
<f:viewParam id="name" name="name" required="true" />
</f:metadata>
The model:
public class ProductBean implements Serializable {
private ProductService productService;
private String name;
private Product product;
public String loadData() {
if(!((name == null) || (name.equals(""))) {
Product product = productService.findByName(name);
this.product = product;
return null;
}
return "error";
}
}
However, there are also many pages with static data, that are not templated in a way described above, using view parameters. These pages simply display what was put in them. For example, there may be many articles that were created as separate views (like /pages/articles/article1.xhtml and so on). Using PrettyFaces we would need to create as many URL mapping as the number of such pages. But, in fact this behavior can also be templated in one URL mapping. Unfortunately, this is not supported in current PrettyFaces release.
The proposed enhancement of the PrettyFaces framework is the following:
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleName }" />
<view-id value="/store/#{ articleName }.xhtml" />
</url-mapping>
or, using an ArticleBean (containing, for example, two fields: articleName and articleId, where name is defined in setter of id field as a unique value):
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleId : articleBean.articleId }" />
<view-id value="/store/#{ articleBean.articleName }.xhtml" />
</url-mapping>
or using other predefined dependence based on an EL-expression, which is in turn based on a unique correspondence.
I want to emphasize that this is not going to be a DynaView because there is no uncertainty in the view-id: there is a one-to-one correspondence between a <pattern> and a <view-id>.
What do you think about implementing this feature in PrettyFaces?
I think Stackoverflow is not the right place to discuss proposals for PrettyFaces. You should have a look at the PrettyFaces Support Forum for that.
There are some options for you to implement something like this. IMHO you could try to do this view DynaView. Even if there is a one-to-one relationship between pattern and view-Id like your wrote. However dynaview has some problems especially with outbound rewriting.
But you should have a look at Rewrite, which is the successor of PrettyFaces. With Rewrite it is very simple to implement such a requirement:
.addRule(Join.path("/articles/{articleName}").to("/store/{articleName}.xhtml"))
Have a look at the configuration examples of Rewrite.
As far as the setup of pretty-config.xml doesn’t currently support this feature, there are some workarounds to achieve this functionality. I will describe them below.
A dummy view with <f:event> that handles navigation to the final pages based on a view parameter in a dummy bean.
URL mapping:
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleName : articleBean.articleName }" />
<view-id value="/handle-article-redirection.xhtml" />
</url-mapping>
View handle-article-redirection.xhtml:
<f:metadata>
<f:viewParam id="articleName" name="articleName" required="true" />
<f:event type="preRenderView" listener="#{articleBean.handleRedirect}" />
</f:metadata>
Model:
public class ArticleBean {
private ArticleService articleService;
private String articleName;
private String articleUrl;
public void handleRedirect() {
if(!((articleName == null) || (articleName.equals(""))) {
String url = articleName;
//String url = articleService.getUrlForArticleName(articleName);
//articleUrl = url;
FacesContext.getCurrentInstance().getExternalContext().redirect("/" + url + ".xhtml");
return null;
}
FacesContext.getCurrentInstance().getExternalContext().redirect("/home.xhtml");
}
}
A meaningful view with a dynamic <ui:include> that imports the necessary page content as a snippet, basing on the bean value / view parameter.
URL mapping:
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleName : articleBean.articleName }" />
<view-id value="/article.xhtml" />
</url-mapping>
View article.xhtml:
<f:metadata>
<f:viewParam id="articleName" name="articleName" required="true" />
</f:metadata>
<h:head></h:head>
<h:body>
<ui:include src="/#{articleBean.articleUrl}.xhtml" />
</h:body>
Model:
public class ArticleBean {
private ArticleService articleService;
private String articleName;
private String articleUrl;
public void setArticleName(String articleName) {
this.articleName = articleName;
if((!(articleName == null)) || (articleName.equals("")) {
articleUrl = articleName;
//articleUrl = articleService.getUrlForArticleName(articleName);
} else {
articleUrl = null;
}
}
}
A DynaView URL mapping with a method that returns a proper outcome.
URL mapping:
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleName : articleBean.articleName }" />
<view-id value="#{articleBean.getViewPath}" />
</url-mapping>
No extra view needed.
Model:
public class ArticleBean {
private ArticleService articleService;
private String articleName;
private String articleUrl;
public String getViewPath() {
this.articleName = articleName;
if(!((articleName == null) || (articleName.equals(""))) {
articleUrl = articleName;
//articleUrl = articleService.getUrlForArticleName(articleName);
return articleUrl;
}
return "error";
}
}
A template view that loads the page data from the database, hence there will be no separate views for those pages.
URL mapping:
<url-mapping id="viewArticle">
<pattern value="/articles/#{ articleName : articleBean.articleName }" />
<view-id value="/article.xhtml" />
<action> #{ articleBean.loadData } </action>
</url-mapping>
View article.xhtml:
<f:metadata>
<f:viewParam id="articleName" name="articleName" required="true" />
</f:metadata>
<h:head></h:head>
<h:body>
<h:panelGroup>
#{articleBean.content}
<h:panelGroup>
</h:body>
Model:
public class ArticleBean {
private ArticleService articleService;
private String articleName;
private String articleUrl;
private String content;
public void loadData() {
if(!((articleName == null) || (articleName.equals(""))) {
articleUrl = articleName;
//articleUrl = articleService.getUrlForArticleName(articleName);
content = articleService.getContentForArticleName(articleName);
} else {
articleUrl = null;
content = null;
}
}
}
Write a custom WebFilter or a NavigationHandler.
What is the best alternative, well, it depends. All of them have their pros and cons.

Mocking ASP.NET MVC Controller properties

I have a MVC controller that loads a resource file and uses Server.MapPath to get the path to the file. I want to mock out the Server property in the controller object using Microsoft Fakes framework (I know how to do this using other frameworks).
Here's the code:
[HttpGet]
public ActionResult GeneratePdf(string reportId)
{
var template = LoadTemplate(reportId);
var document = pdfWriter.Write(GetReportModel(reportId), template);
return File(document, MediaTypeNames.Application.Pdf);
}
private byte[] LoadTemplate(string reportId)
{
var templatePath = Server.MapPath(string.Format("~/ReportTemplates/{0}.docx", reportId));
using(var templateContent = System.IO.File.OpenText(templatePath))
{
return Encoding.Default.GetBytes(templateContent.ReadToEnd());
}
}
The part I'm trying to mock out is the "Server.MapPath" method.
As of Visual Studio 2012 Update 1, you can detour the Controller.Server property using Stubs.
With the following .Fakes file in your test project:
<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
<Assembly Name="System.Web" Version="4.0.0.0"/>
<StubGeneration>
<Clear/>
<Add FullName="System.Web.HttpContextBase!"/>
<Add FullName="System.Web.HttpServerUtilityBase!"/>
</StubGeneration>
<ShimGeneration>
<Clear/>
</ShimGeneration>
</Fakes>
You can write the a unit test like this:
[TestMethod]
public void TestMethod1()
{
var target = new TestController();
var serverStub = new StubHttpServerUtilityBase();
serverStub.MapPathString = (path) => path.Replace("~", string.Empty).Replace("/", #"\");
var contextStub = new StubHttpContextBase();
contextStub.ServerGet = () => serverStub;
target.ControllerContext = new ControllerContext();
target.ControllerContext.HttpContext = contextStub;
var result = (FilePathResult) target.Index();
Assert.AreEqual(#"\Content\Test.txt", result.FileName);
}
With the upcoming Update 2, you will also be able to detour Controller.Server property directly using Shims. Here is the additional .Fakes file you will need with this approach.
<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
<Assembly Name="System.Web.Mvc" Version="4.0.0.0"/>
<StubGeneration>
<Clear/>
</StubGeneration>
<ShimGeneration>
<Clear/>
<Add FullName="System.Web.Mvc.Controller!"/>
</ShimGeneration>
</Fakes>
And here is the test:
[TestMethod]
public void TestMethod2()
{
using (ShimsContext.Create())
{
var target = new TestController();
var serverStub = new StubHttpServerUtilityBase();
serverStub.MapPathString = (path) => path.Replace("~", string.Empty).Replace("/", #"\");
var controllerShim = new ShimController(target);
controllerShim.ServerGet = () => serverStub;
var result = (FilePathResult)target.Index();
Assert.AreEqual(#"\Content\Test.txt", result.FileName);
}
}
Please note that this approach does not work in the current version (Update 1), due to limitations in Fakes runtime related to assemblies, like System.Web.Mvc that allow partially trusted callers. If you try to run the second test today, you will get a VerificationException.