I'd like to create a custom 404 page in akka-http (high level DSL). This basically means:
Return a page from my static folder (e.g. resources/www/404.html)
Set the result code to ResultCodes.NOT_FOUND
What I tried so far:
getFromResource - I can return the entity, but I can't figure out how to override the HTTP result code for the response, so I can set it to '404'.
complete() - I can return the right code, but I need to read the html page in manually, and build the HttpResponse from ground up. It eventually works, but it's a bit cumbersome.
Am I missing something? Is there an easier way to return a page and customize the result code?
The static page can be returned as the entity of an HttpResponse.
Assuming you have some function of the form
def someFunctionThatCanFail() : Try[HttpResponse] = ???
You will want to use your static page in the event of a failure. You'll first need to create a Source that is based on the static page:
import akka.stream.scaladsl._
import akka.http.scaladsl.model.HttpEntity.Chunked
def createStaticSource(fileName : String) =
FileIO
.fromPath(Paths get fileName)
.map(ChunkStreamPart.apply)
def createChunkedSource(fileName : String) =
Chunked(ContentTypes.`text/html(UTF-8)`, createStaticSource(fileName))
This source can then be placed inside of a response:
def staticResponse =
HttpResponse(status = StatusCodes.NotFound,
entity = createChunkedSource("resources/www/404.html"))
The only thing left to do is to either return the result of the function if it was valid or the static response in the case of a failure:
val route =
get {
complete(someFunctionThatCanFail() getOrElse staticResponse)
}
To expand on Ramon's excellent answer, this works inside a jar file as well:
def createChunkedSource(fileName : String): Chunked = {
def createStaticSource(fileName : String) : Source[ChunkStreamPart, Any] = {
val classLoader = getClass.getClassLoader
StreamConverters.fromInputStream(() => classLoader.getResourceAsStream(fileName)).map(ChunkStreamPart.apply)
}
Chunked(ContentTypes.`text/html(UTF-8)`, createStaticSource(fileName))
}
Related
I'm a bit lost trying to attach a pdf with populated values from an opportunity record
Here is the code:
Trigger:
trigger OpportunityTrigger on Opportunity (after insert)
if(trigger.isAfter && trigger.isUpdate) {
opportunityTriggerHelper.attachFileToOpportunityRecord(trigger.new);
}
Helper Class:
private void attachFileToOpportunityRecord(List<Opportunity> lstOpp) {
List<Id> oppListIdsForAttach = new List<Id>();
for(Opportunity opp : lstOpp) {
oppListIdsForAttach .add(opp.Id);
}
attachFileToOpportunities(oppListIdsForAttach);
}
#future(callout=true)
private static void attachFileToOppotunities(List<Id> OpportunityIds) {
List<Attachment> attachList = new List<Attachment>();
for(Id oppId : opportunityIds) {
OpportunityPdfController file = new OpportunityPdfController();
file.getData(oppId);
PageReference pdfPage = Page.PdfAttachmentForOpp;
blob pdfBody;
pdfBody = pdfPage.getContent();
Attachment attach = new Attachment();
attach.Body = pdfBody;
attach.Name = 'Pdf file';
attach.IsPrivate = false;
attach.ParenId = oppId;
attachList.add(attach);
}
insert attachList;
}
VF Page:
<apex:page controller="OpportunityPdfController" renderAs="pdf">
<apex:repeat value="{!pricingDetails}" var="pd">
<apex:outputText>{!pd.basePrice}</apex:outputText>
</apex:repeat>
</apex:page>
VF Page Controller:
public with sharing class OpportunityPdfController {
public List<PricingDetailWrapper> pricingDetails {get;set;}
public void getData(Id opportunityId) {
List<Pricing_Detail__c> pdList = [
SELECT basePrice
FROM Pricing_Detail__c
WHERE OpportunityId =: opportunityId
];
for(Pricing_Detail__c pd : pdList) {
PricingDetailWrapper pdw = new PricingDetailWrapper();
pdw.basePrice = pd.basePrice;
pricingDetails.add(pdw);
}
}
public class PricingDetailWrapper {
public String basePrice {get;set;}
}
}
The result is whenever I update an opportunity it attaches the corresponding pdf file but it is blank and if I add for example the following to vf page body: "<h1> hello World!</h1>" this works and shows as expected, but this is not happening to what I required above.
You didn't really pass the opportunity id to the VF page. And I doubt this actually works at all? If you manually access the VF page as /apex/PdfAttachmentForOpp?id=006... does it render the content ok? I'm assuming it doesn't.
Fixing the page
You didn't specify constructor so SF generates one for you, fine. I think you need to add something like
public OpportunityPdfController(){
if(ApexPages.currentPage() != null){
Id oppId = ApexPages.currentPage().getParameters().get('id');
System.debug(oppId);
getData(oppId);
}
}
Add this, try to access the page passing valid opp id and see if it renders ok, if right stuff shows in debug log. /apex/PdfAttachmentForOpp?id=006...
(VF page constructors are bigger topic, this might be simpler with standardController + extension class)
Fixing the callout
VF page (especially accessed as callout) will not share memory with the OpportunityPdfController controller you've created in the code. New object of this class will be created to support the page and your file will be ignored. You might try to make-do with some static variable holding current opportunity's id but it feels bit yucky.
In normal execute anonymous try if this returns correct pdf:
PageReference pdfPage = Page.PdfAttachmentForOpp;
pdfPage.getParameters().put('id', '006...');
Blob pdfBody = pdfPage.getContent();
System.debug(pdfBody.toString());
If it works - use similar trick in the actual code, pass the id as url parameter.
I manage status codes error using
app.UseStatusCodePagesWithRedirects("/StatusCodeError/{0}");
But when an error occurs in the area, the page is redirected out of the area and shows the general error page.
How do I make sure that an area-specific error page is displayed if an error occurs in the area?
There is no extensibility point to modify the behavior of app.UseStatusCodePagesWithRedirects, the location is formatted internally using string.Format and with only one argument passed in the placeholder {0} for the status code. So at the calling time (passing in the location format), we have no understanding about the area which is a route value available only in the context of a request processing. So you can refer to the source for StatusCodePagesExtensions and can see that UseStatusCodePagesWithRedirects just depends on an overload of the extension method StatusCodePagesExtensions.UseStatusCodePages, you can freely copy the code and modify it to make another version of UseStatusCodePagesWithRedirects that accepts a location format which supports some arguments from the route values.
Here I've made a simple one for you:
public static class StatusCodePagesApplicationBuilderExtensions
{
public static IApplicationBuilder UseStatusCodePagesWithRouteDataAndRedirects(this IApplicationBuilder app, string locationFormat)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
var shouldPrependPathBase = locationFormat.StartsWith("~");
locationFormat = shouldPrependPathBase ? locationFormat.Substring(1) : locationFormat;
return app.UseStatusCodePages(context =>
{
//here is the point we can evaluate for the actual location
//by replacing the placeholders with route value
var location = Regex.Replace(locationFormat, #"\{[^}]+\}", m => {
//ignore the placeholder for status code
if (m.Value == "{0}") return m.Value;
var routeKey = m.Value.Trim('{', '}');
var routeValue = context.HttpContext.GetRouteValue(routeKey);
return routeValue?.ToString();
});
location = string.Format(CultureInfo.InvariantCulture, location, context.HttpContext.Response.StatusCode);
location = (shouldPrependPathBase ? context.HttpContext.Request.PathBase : PathString.Empty) + location;
context.HttpContext.Response.Redirect(location);
return Task.CompletedTask;
});
}
}
Now you can include any route data in the location format, in your case you just need the area, so the code will be like this:
app.UseStatusCodePagesWithRouteDataAndRedirects("/{area}/StatusCodeError/{0}");
NOTE: be careful with redirecting, you may encounter an error saying something like too many redirects, that's because of redirecting to a URL which in return causes a loop of redirects.
I have a problem in my Camel Route when I try to check the value of a header.
So what is happening is that I go to a processor, do my stuff, then I create 2 different message that I put inside the body.
After that I go back on my route, I split my body so I can route the 2 differents messages, and there I use a .choice().when() on the header CamelFileName to check if it contains some value.
It doesn't find the value and then doesn't go inside the .when()
Here is the source code to make it more clear :
// My route
from("myQuartz")
.routeId("myId")
.bean(myProcessor.class)
.split(body())
.to("log:test?showAll=true&multiline=true")
.log('[${header.CamelFileName}]')
.choice()
.when(header('CamelFileName').contains('myString1'))
// do my stuff
.endChoice()
.when(header('CamelFileName').contains('myString2'))
// do my other stuff
.endChoice()
.otherwise()
.log("It did not go inside the when")
.to("log:test?showAll=true&multiline=true")
.endChoice()
.end()
So here I am simply trying to check if the CamelFileName header contains a string (it's not a variable), but it keep going inside the otherwise.
The log just after the split show me that the CamelFileName header is correct and do contains the string I am looking for.
I tried different ways of checking the value inside the when() such as using a simple(), but it doesn't work.
My file is a groovy file.
Thanks for your help.
EDIT :
So to explain what is inside my body I will show you the processor source code.
I create two DefaultMessage,
I set them a body and a CamelFileName header,
I put them into a list and then I put that list into my exchange body.
After that I go back to the route and I split the body so it will separate the two messages and route them.
Here is what's happening in my processor :
// Message 1
DefaultMessage message1 = new DefaultMessage()
message1.setBody(bodyContent)
def fileName1 = "myString1_blablabla.txt"
message1.setHeader("CamelFileName",fileName1)
listMessages.add(message1)
// Message 2
DefaultMessage message2 = new DefaultMessage()
message2.setBody(bodyContent)
def fileName2 = "myString2_blablabla.txt"
message2.setHeader("CamelFileName",fileName2)
listMessages.add(message2)
exchange.in.setBody(listMessages)
I've setup a simpler test for your route. It routes data to the proper when clause. When you split(), the headers get copied for each exchange, so I'm not sure why you would expect (given your route) why the elements of the list would have different header values.
public class SampleTest extends CamelTestSupport{
#Override
protected RouteBuilder createRouteBuilder() throws Exception {
return new RouteBuilder() {
#Override
public void configure() throws Exception {
from("direct:start")
.setHeader("CamelFileName", simple("myString1"))
.split(body())
.choice()
.when(header("CamelFileName").contains("myString1"))
.to("mock:myString1")
.endChoice()
.when(header("CamelFileName").contains("myString2"))
.to("mock:myString2")
.endChoice()
.otherwise()
.to("mock:otherwise")
.endChoice()
.end();
}
};
}
#Test
public void test() throws Exception {
//Setup mock body
java.util.List<String> myList = new java.util.ArrayList<String>();
myList.add("1");
myList.add("2");
MockEndpoint mock1 = getMockEndpoint("mock:myString1");
MockEndpoint mock2 = getMockEndpoint("mock:myString2");
MockEndpoint mockOtherwise = getMockEndpoint("mock:otherwise");
mock1.expectedMessageCount(myList.size());
mock2.expectedMessageCount(0);
mockOtherwise.expectedMessageCount(0);
template.sendBody("direct:start", myList);
assertMockEndpointsSatisfied();
}
}
The plugin play-reactivemongo offers an easy way to upload a file:
def upload = Action(gridFSBodyParser(gridFS)) { request =>
val futureFile: Future[ReadFile[BSONValue]] = request.body.files.head.ref
futureFile.map { file =>
// do something
Ok
}.recover { case e: Throwable => InternalServerError(e.getMessage) }
}
Unfortunately this solution doesn't suit me because:
I would like only my DAO layer to depend on reactive-mongo.
I need to save the file only if a user is authenticated (with SecureSocial) and use some user's properties as checks and metadata.
If no user is authenticated the request body shouldn't be parsed at all (see also this question).
It would be something along the lines
def upload = SecuredAction { request =>
val user = request.user
val enumerator = an enumrator from the body parsing ???
myDAO.saveFile(user, enumerator)
object myDAO {
def saveFile(user:User, enumerator:Enumerator[Array[Byte]]) = {
...
val fileToSave = DefaultFileToSave(...)
gridfs.save(enumerator, fileToSave)
...
}
}
Unfortunately it seems there is no way to get an enumerator from the parsing of the request body. The only way seems to provide the Action with a parser and an Iteratee that will be fed with the the body being parsed.
I couldn't figure out how to achieve it in a reactive way (without using a temporary file or storing the body in memory). Is it at all possible?
Actually, you might consider not using girdFS built-in parser at all:
val gfs = new GridFS(db)
// the controller method, Authenticated here is custom object extending ActionBuilder
def upload = Authenticated.async(parse.multipartFormData) { request =>
...
request.body.file("photo") match {
// handle error cases
...
case Some(photo) =>
val fileToSave = DefaultFileToSave(photo.filename, photo.contentType)
// here some more operations, basically you don't need the and need only photo.ref.file
val enumerator = Enumerator(Image(photo.ref.file).fitToWidth(120).write)
gfs.save(enumerator, fileToSave) map {
//handle responses and stuff
...
}
}
}
}
public function testheaders()
{
$url=$this->url('http://www.example.com/index.php');
$kur = get_headers($url);
var_dump($kur);
}
I got the error : get_headers(): Filename cannot be empty.
My class extends PHPUnit_Extensions_Selenium2TestCase like my all my other tests.
get_headers() is a PHP function that will give you the headers return by an HTTP request to a specified url. The parameter you need to give is a string, but $this->url() return an PHPUnit_Selenium-object.
If you want the headers of that known URL, why not do directly?
$kur = get_headers('http://www.example.com/index.php');