Spring data elasticSearch page no totalpage - spring-data

springBootVersion = '2.0.0.RC2'
compile('org.springframework.boot:spring-boot-starter-data-elasticsearch')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
Page findDistinctEsBlogByTitleContainingOrSummaryContainingOrContentContainingOrTagsContaining(String title, String summary, String content, String tags, Pageable pageable);
int totalPages = page.getTotalPages();
System.out.println(totalPages);
totalPages alwarys is 1

Related

Getting the last message on a Gmail message thread in Katalon Studio

I'm using this plugin for Katalon Studio to access the last unread message from my testing Gmail account.
My email util class is like:
public final class SMDEmailUtils {
public static final String MainInboxFolder = "INBOX";
public static final String SpamFolder = "[Gmail]/Spam";
public static String GetMainEmail() {
if (!GeneralWebUIUtils.GlobalVariableExists('emailID'))
return "dev#example.com";
return GlobalVariable.emailID.toString();
}
public static String ExtractSignUpLink() {
final String folderName = this.GetNewMessageFolderName(30, FailureHandling.STOP_ON_FAILURE);
return this.ProcessHTML(this.GetNewMessage(folderName), "//a[.//div[#class = 'sign-mail-btn-text']]/#href");
}
public static String GetNewMessageFolderName(int timeOut,
FailureHandling failureHandling = FailureHandling.STOP_ON_FAILURE) {
final long startTime = System.currentTimeMillis()
final Map<String, Integer> folderMessageCountDict = [
(this.MainInboxFolder) : this.GetMessageCount(this.MainInboxFolder),
(this.SpamFolder) : this.GetMessageCount(this.SpamFolder),
];
while (System.currentTimeMillis() < startTime + 1000 * timeOut) {
final String folderName = folderMessageCountDict.findResult({String folderName, int initialMessageCount ->
if (initialMessageCount < this.GetMessageCount(folderName))
return folderName;
return null;
})
if (folderName != null)
return folderName;
// TODO: we shouldn't have to do some hard-coded suspension of the runtime. We need to close the store somehow
Thread.sleep(1000 * 2);
}
throw new StepFailedException("Failed to find a folder with a new message in it after ${(System.currentTimeMillis() - startTime) / 1000} seconds");
}
public static int GetMessageCount(String folderName) {
return Gmail.getEmailsCount(this.GetMainEmail(), GlobalVariable.emailPassword, folderName);
}
public static String GetNewMessage(String folderName) {
return Gmail.readLatestEMailBodyContent(this.GetMainEmail(), GlobalVariable.emailPassword, folderName);
}
/**
* **NOTE**: forked from https://stackoverflow.com/a/2269464/2027839 , and then refactored
*
* Processes HTML, using XPath
*
* #param html
* #param xpath
* #return the result
*/
public static String ProcessHTML(String html, String xpath) {
final String properHTML = this.ToProperHTML(html);
final Element document = DocumentBuilderFactory.newInstance()
.newDocumentBuilder()
.parse(new ByteArrayInputStream( properHTML.bytes ))
.documentElement;
return XPathFactory.newInstance()
.newXPath()
.evaluate( xpath, document );
}
private static String ToProperHTML(String html) {
// SOURCE: https://stackoverflow.com/a/19125599/2027839
String properHTML = html.replaceAll( "(&(?!amp;))", "&" );
if (properHTML.contains('<!DOCTYPE html'))
return properHTML;
return """<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head></head>
<body>
${properHTML}
</body>
</html>
""";
}
}
My use case of that is the following:
a test member lead, whose email forwards to my testing email ([myTestingEmailName]+[memberLeadName]#gmail.com), gets a link to an agreement to sign
on successful signature, the physician, whose email also forwards to my testing email ([myTestingEmailName]+[physicianName]#gmail.com), gets a link to an agreement to sign
Step 1 works, the link gets extracted successfully via SMDEmailUtils.ExtractSignUpLink() .
However, when it is the physician's turn to sign, that same line of code doesn't work. It's giving me the link from the first email message (the one meant for the recipient in step 1, that was already signed).
I check out my inbox manually, and see this:
The AUT sent both email messages on the same thread, but the plugin can only handle the first message on the thread!
How do I handle this?

mybatis passing in the datatype on dynamic update query

I am trying to create a dynamic update statement using dynamic-sql method and sql-builder method but I only manage it get it work for string datatype. I'm not exactly sure how to "cast" to the correct datatype when constructing the update statement.
What I want to achieve is to generate the update statement using Map<String, Object> or the actual pojo Post
Post look like this
public class Post {
private Integer id;
private String title;
private String body;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
}
Reason for Map<String, Object> is so that it's easier to iterate through the collection and construct the statement. Using the pojo would require me to use reflection which I try not to.
Before getting into how I did it
This is how when using a normal update statement with the pojo looks like
#PutMapping("/{id}")
public Post updateById(#PathVariable Integer id, #RequestBody Post post) {
return this.postService.updateById(id, post);
}
#Update("UPDATE POST SET title = #{p.title}, body = #{p.body}, createdAt = #{p.createdAt}, createdBy = #{p.createdBy}, updatedAt = #{p.updatedAt}, updatedBy = #{p.updatedBy} WHERE id = #{id}")
public boolean updateById(#Param("id") Integer id, #Param("p") Post post);
That would result in
2021-10-30 12:03:15.037 DEBUG 15988 --- [nio-8080-exec-2] c.b.s.s.post.PostMapper.updateById : ==> Preparing: UPDATE POST SET title = ?, body = ?, createdAt = ?, createdBy = ?, updatedAt = ?, updatedBy = ? WHERE id = ?
2021-10-30 12:03:15.064 DEBUG 15988 --- [nio-8080-exec-2] c.b.s.s.post.PostMapper.updateById : ==> Parameters: jsonpatch1(String), bo21(String), 2021-10-30T12:03:14.954483(LocalDateTime), stackoverflow(String), 2021-10-30T12:03:14.954483(LocalDateTime), stackoverflow(String), 65(Integer)
So with that, I tried to do this
// What this does is to strip off all the null values, and keep only those with value
// and convert into a map to pass and run in the dynamic sql later
#PatchMapping(path = "/{id}")
public Post patchById(#PathVariable Integer id, #RequestBody Post post) {
ObjectMapper om = new ObjectMapper();
om.setSerializationInclusion(Include.NON_NULL);
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
Map<String, Object> mp = om.convertValue(post, new TypeReference<Map<String, Object>>(){});
return this.postService.patchById(id, mp);
}
Where it goes to a mapper that looks something like this
#Update({
"<script>",
"UPDATE POST",
"<set>",
"<foreach item='item' index='index' collection='p.entrySet()'>",
"${index} = #{item},",
"</foreach>",
"</set>",
"WHERE id = #{id}",
"</script>"
})
public boolean update(#Param("id") Integer id, #Param("p") Map<String, Object> post);
This works if all the values are string. However, if there is a field of LocalDateTime createdAt, the createdAt field is deem as a string type
021-10-30 15:21:27.666 DEBUG 12324 --- [nio-8080-exec-2] c.b.s.s.post.PostUpdateMapper.update : ==> Preparing: UPDATE POST SET createdAt = ?, title = ?, body = ?
WHERE id = ?
2021-10-30 15:21:27.669 DEBUG 12324 --- [nio-8080-exec-2] c.b.s.s.post.PostUpdateMapper.update : ==> Parameters: 2021-09-10T11:31:07.5306869(String), jsonpatch1(String), bo221(String), 65(Integer)
I believe, that is because I switch it to Map<String, Object> and hence the type (LocalDateTime) is loss with the conversion. However, if I were to do it using the pojo Bean
I would have something like this
#PatchMapping(path = "/{id}")
public Post patchById(#PathVariable Integer id, #RequestBody Post post) {
return this.postService.patchById(id, post);
}
#UpdateProvider(type=SQLUpdate.class, method = "update")
public boolean update(Integer id, Post post);
// just a poc to see if it works
public String update(Integer id, Post post) throws IllegalArgumentException, IllegalAccessException {
Field[] f = post.getClass().getDeclaredFields();
return new SQL() {{
UPDATE("POST");
for(Field field: f) {
field.setAccessible(true);
if (field.get(post) != null) {
SET(field.getName() + " = '" + field.get(post) + "'");
}
}
WHERE("id = " + id);
}}.toString();
}
So either way, I'm not sure how to pass in the correct type so that it can intercept and run correctly
This would be the more ideal solution if I can achieve this
#Update({
"<script>",
"UPDATE POST",
"<set>",
// being able to check if the value is null and set the field and value dynamically
"<if test='#{p.value} != null>p.fieldname = #{p.value}",
"</set>",
"WHERE id = #{id}",
"</script>"
})
public boolean update(#Param("id") Integer id, #Param("p") Post post);
Let me know if more information is needed, or if there is a better way to achieve what I want to do
Thanks!
P.S: I know and I got it working with mybatis-dynamic-sql lib but interested to know if it cab work without using the lib

Replacement for "GROUP BY" in ContentResolver query in Android Q ( Android 10, API 29 changes)

I'm upgrading some legacy to target Android Q, and of course this code stop working:
String[] PROJECTION_BUCKET = {MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA,
"COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS COUNT",
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns._ID};
String BUCKET_GROUP_BY = " 1) and " + BUCKET_WHERE.toString() + " GROUP BY 1,(2";
cur = context.getContentResolver().query(images, PROJECTION_BUCKET,
BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
android.database.sqlite.SQLiteException: near "GROUP": syntax error (code 1 SQLITE_ERROR[1])
Here it supposed to obtain list of images with album name, date, count of pictures - one image for each album, so we can create album picker screen without querying all pictures and loop through it to create albums.
Is it possible to group query results with contentResolver since SQL queries stoped work?
(I know that ImageColumns.DATA and "COUNT() AS COUNT" are deprecated too, but this is a question about GROUP BY)
(There is a way to query albums and separately query photo, to obtain photo uri for album cover, but i want to avoid overheads)
Unfortunately Group By is no longer supported in Android 10 and above, neither any aggregated functions such as COUNT. This is by design and there is no workaround.
The solution is what you are actually trying to avoid, which is to query, iterate, and get metrics.
To get you started you can use the next snipped, which will resolve the buckets (albums), and the amount of records in each one.
I haven't added code to resolve the thumbnails, but is easy. You must perform a query for each bucket Id from all the Album instances, and use the image from the first record.
public final class AlbumQuery
{
#NonNull
public static HashMap<String, AlbumQuery.Album> get(#NonNull final Context context)
{
final HashMap<String, AlbumQuery.Album> output = new HashMap<>();
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final String[] projection = {MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID};
try (final Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null))
{
if ((cursor != null) && (cursor.moveToFirst() == true))
{
final int columnBucketName = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
final int columnBucketId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID);
do
{
final String bucketId = cursor.getString(columnBucketId);
final String bucketName = cursor.getString(columnBucketName);
if (output.containsKey(bucketId) == false)
{
final int count = AlbumQuery.getCount(context, contentUri, bucketId);
final AlbumQuery.Album album = new AlbumQuery.Album(bucketId, bucketName, count);
output.put(bucketId, album);
}
} while (cursor.moveToNext());
}
}
return output;
}
private static int getCount(#NonNull final Context context, #NonNull final Uri contentUri, #NonNull final String bucketId)
{
try (final Cursor cursor = context.getContentResolver().query(contentUri,
null, MediaStore.Images.Media.BUCKET_ID + "=?", new String[]{bucketId}, null))
{
return ((cursor == null) || (cursor.moveToFirst() == false)) ? 0 : cursor.getCount();
}
}
public static final class Album
{
#NonNull
public final String buckedId;
#NonNull
public final String bucketName;
public final int count;
Album(#NonNull final String bucketId, #NonNull final String bucketName, final int count)
{
this.buckedId = bucketId;
this.bucketName = bucketName;
this.count = count;
}
}
}
This is a more efficient(not perfect) way to do that.
I am doing it for videos, but doing so is the same for images to. just change MediaStore.Video.Media.X to MediaStore.Images.Media.X
public class QUtils {
/*created by Nasib June 6, 2020*/
#RequiresApi(api = Build.VERSION_CODES.Q)
public static ArrayList<FolderHolder> loadListOfFolders(Context context) {
ArrayList<FolderHolder> allFolders = new ArrayList<>();//list that we need
HashMap<Long, String> folders = new HashMap<>(); //hashmap to track(no duplicates) folders by using their ids
String[] projection = {MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DATE_ADDED};
ContentResolver CR = context.getContentResolver();
Uri root = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
Cursor c = CR.query(root, projection, null, null, MediaStore.Video.Media.DATE_ADDED + " desc");
if (c != null && c.moveToFirst()) {
int folderIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID);
int folderNameIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
int thumbIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int dateAddedIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED);
do {
Long folderId = c.getLong(folderIdIndex);
if (folders.containsKey(folderId) == false) { //proceed only if the folder data has not been inserted already :)
long thumbId = c.getLong(thumbIdIndex);
String folderName = c.getString(folderNameIndex);
String dateAdded = c.getString(dateAddedIndex);
Uri thumbPath = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, thumbId);
folders.put(folderId, folderName);
allFolders.add(new FolderHolder(String.valueOf(thumbPath), folderName, dateAdded));
}
} while (c.moveToNext());
c.close(); //close cursor
folders.clear(); //clear the hashmap becuase it's no more useful
}
return allFolders;
}
}
FolderHolder model class
public class FolderHolder {
private String folderName;
public long dateAdded;
private String thumbnailPath;
public long folderId;
public void setPath(String thumbnailPath) {
this.thumbnailPath = thumbnailPath;
}
public String getthumbnailPath() {
return thumbnailPath;
}
public FolderHolder(long folderId, String thumbnailPath, String folderName, long dateAdded) {
this.folderId = folderId;
this.folderName = folderName;
this.thumbnailPath = thumbnailPath;
this.dateAdded = dateAdded;
}
public String getFolderName() {
return folderName;
}
}
GROUP_BY supporting in case of using Bundle:
val bundle = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
)
putString(
ContentResolver.QUERY_ARG_SQL_GROUP_BY,
MediaStore.Images.ImageColumns.BUCKET_ID
)
}
contentResolver.query(
uri,
arrayOf(
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA
),
bundle,
null
)

Show Page number range of subreports in Master Report

I have a master report with 4 subreports in its details section. I want to display the page number range (for example 1- 2) of all the subreports in the Master report title section. I tried using subreport return value but it works only when I have 1 subreport not if there are more than 1 subreport
Here is the solution
Step1 : Create variables of any type inside each of the SubReport and the set the calculation, Increment Type and Reset Type to None. No need to specify the Expression as well Initial Value Expression
Step 2 : Create variables of String type inside the Master Report to hold the value of page number (eg. 1-2) and set the Calculation as System.
Step 3 : Add the variables created in Step2 on the Master Report Title section and set the evaluation time as Report.
Step 4 : Create scriptlet and set the page number in that
public class PageScriptlet extends JRDefaultScriptlet {
private static int mgmtReportPages;
private static int balanceSheetReportPages;
private static int incomeStmtReportPages;
private static int addInfoReportPages;
private static final String MGMT_REPORT_PAGENUM = "mgmtReportPageNum";
private static final String INCOME_STMT_PAGENUM = "incomeStmtPageNum";
private static final String BALANCE_SHEET_PAGENUM = "balanceSheetPageNum";
private static final String ADDINFO_PAGENUM = "addInfoPageNum";
private static final String MGMT_REPORT_INDEX = "mgmtReportIndex";
private static final String INCOMESTMT_INDEX = "incomeStmtIndex";
private static final String BALANCESHEET_INDEX = "balanceSheetIndex";
private static final String NOTES_INDEX = "notesIndex";
private static final String SIGNATURE_INDEX = "signatureIndex";
private static Integer firstPage = 2;
public PageScriptlet() {
super ();
}
#Override
public void afterPageInit() throws JRScriptletException{
Map<String, JRFillVariable> variablesMap = this.variablesMap;
if(variablesMap.containsKey(MGMT_REPORT_INDEX)){
Integer lastPage = mgmtReportPages+1;
String index = null;
if(firstPage == lastPage){
index = String.valueOf(firstPage);
}else{
index = String.valueOf(firstPage)+"-"+String.valueOf(lastPage);
}
this.setVariableValue(MGMT_REPORT_INDEX, index);
}
if(variablesMap.containsKey(INCOMESTMT_INDEX)){
Integer firstPage = mgmtReportPages + 2;
Integer lastPage = incomeStmtReportPages;
String index = null;
if(firstPage == lastPage){
index = String.valueOf(firstPage);
}else{
index = String.valueOf(firstPage)+"-"+String.valueOf(lastPage);
}
this.setVariableValue(INCOMESTMT_INDEX, index);
}
if(variablesMap.containsKey(BALANCESHEET_INDEX)){
Integer firstPage = incomeStmtReportPages+1;
Integer lastPage = balanceSheetReportPages;
String index = null;
if(firstPage == lastPage){
index = String.valueOf(firstPage);
}else{
index = String.valueOf(firstPage)+"-"+String.valueOf(lastPage);
}
this.setVariableValue(BALANCESHEET_INDEX, index);
}
if(variablesMap.containsKey(NOTES_INDEX)){
Integer firstPage = balanceSheetReportPages + 1;
Integer lastPage = addInfoReportPages;
String index = null;
if(firstPage == lastPage){
index = String.valueOf(firstPage);
}else{
index = String.valueOf(firstPage)+"-"+String.valueOf(lastPage);
}
this.setVariableValue(NOTES_INDEX, index);
}
if(variablesMap.containsKey(SIGNATURE_INDEX)){
Integer lastPage = addInfoReportPages;
String index = String.valueOf(lastPage);
this.setVariableValue(SIGNATURE_INDEX, index);
}
}
#Override
public void beforePageInit() throws JRScriptletException{
Map<String, JRFillVariable> variablesMap = this.variablesMap;
Integer pageNumber = (Integer)this.getVariableValue("PAGE_NUMBER");
if(variablesMap.containsKey(MGMT_REPORT_PAGENUM)){
mgmtReportPages = pageNumber == null ? 1 : pageNumber + 1;
}
if(variablesMap.containsKey(INCOME_STMT_PAGENUM)){
incomeStmtReportPages = pageNumber == null ? mgmtReportPages + 2 : incomeStmtReportPages + 1;
}
if(variablesMap.containsKey(BALANCE_SHEET_PAGENUM)){
balanceSheetReportPages = pageNumber == null ? incomeStmtReportPages + 1 : balanceSheetReportPages + 1;
}
if(variablesMap.containsKey(ADDINFO_PAGENUM)){
addInfoReportPages = pageNumber == null ? balanceSheetReportPages+1 : addInfoReportPages + 1;
}
}
}

Serve video file to iPhone from ASP.NET MVC2

I'm attempting to serve video files from ASP.NET MVC to iPhone clients. The video is formatted properly, and if I have it in a publicly accessible web directory it works fine.
The core issue from what I've read is that the iPhone requires you to have a resume-ready download environment that lets you filter your byte ranges through HTTP headers. I assume this is so that users can skip forward through videos.
When serving files with MVC, these headers do not exist. I've tried to emulate it, but with no luck. We have IIS6 here and I'm unable to do many header manipulations at all. ASP.NET will complain at me saying "This operation requires IIS integrated pipeline mode."
Upgrading isn't an option, and I'm not allowed to move the files to a public web share. I feel limited by our environment but I'm looking for solutions nonetheless.
Here is some sample code of what I'm trying to do in short...
public ActionResult Mobile(string guid = "x")
{
guid = Path.GetFileNameWithoutExtension(guid);
apMedia media = DB.apMedia_GetMediaByFilename(guid);
string mediaPath = Path.Combine(Transcode.Swap_MobileDirectory, guid + ".m4v");
if (!Directory.Exists(Transcode.Swap_MobileDirectory)) //Make sure it's there...
Directory.CreateDirectory(Transcode.Swap_MobileDirectory);
if(System.IO.File.Exists(mediaPath))
return base.File(mediaPath, "video/x-m4v");
return Redirect("~/Error/404");
}
I know that I need to do something like this, however I'm unable to do it in .NET MVC. http://dotnetslackers.com/articles/aspnet/Range-Specific-Requests-in-ASP-NET.aspx
Here is an example of an HTTP response header that works:
Date Mon, 08 Nov 2010 17:02:38 GMT
Server Apache
Last-Modified Mon, 08 Nov 2010 17:02:13 GMT
Etag "14e78b2-295eff-4cd82d15"
Accept-Ranges bytes
Content-Length 2711295
Content-Range bytes 0-2711294/2711295
Keep-Alive timeout=15, max=100
Connection Keep-Alive
Content-Type text/plain
And here is an example of one that doesn't (this is from .NET)
Server ASP.NET Development Server/10.0.0.0
Date Mon, 08 Nov 2010 18:26:17 GMT
X-AspNet-Version 4.0.30319
X-AspNetMvc-Version 2.0
Content-Range bytes 0-2711294/2711295
Cache-Control private
Content-Type video/x-m4v
Content-Length 2711295
Connection Close
Any ideas? Thank you.
UPDATE: This is now a project on CodePlex.
Okay, I got it working on my local testing station and I can stream videos to my iPad. It's a bit dirty because it was a little more difficult than I expected and now that it's working I don't have the time to clean it up at the moment. Key parts:
Action Filter:
public class ByteRangeRequest : FilterAttribute, IActionFilter
{
protected string RangeStart { get; set; }
protected string RangeEnd { get; set; }
public ByteRangeRequest(string RangeStartParameter, string RangeEndParameter)
{
RangeStart = RangeStartParameter;
RangeEnd = RangeEndParameter;
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
if (!filterContext.ActionParameters.ContainsKey(RangeStart))
filterContext.ActionParameters.Add(RangeStart, null);
if (!filterContext.ActionParameters.ContainsKey(RangeEnd))
filterContext.ActionParameters.Add(RangeEnd, null);
var headerKeys = filterContext.RequestContext.HttpContext.Request.Headers.AllKeys.Where(key => key.Equals("Range", StringComparison.InvariantCultureIgnoreCase));
Regex rangeParser = new Regex(#"(\d+)-(\d+)", RegexOptions.Compiled);
foreach(string headerKey in headerKeys)
{
string value = filterContext.RequestContext.HttpContext.Request.Headers[headerKey];
if (!string.IsNullOrEmpty(value))
{
if (rangeParser.IsMatch(value))
{
Match match = rangeParser.Match(value);
filterContext.ActionParameters[RangeStart] = int.Parse(match.Groups[1].ToString());
filterContext.ActionParameters[RangeEnd] = int.Parse(match.Groups[2].ToString());
break;
}
}
}
}
public void OnActionExecuted(ActionExecutedContext filterContext)
{
}
}
Custom Result based on FileStreamResult:
public class ContentRangeResult : FileStreamResult
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public long TotalSize { get; set; }
public DateTime LastModified { get; set; }
public FileStreamResult(int startIndex, int endIndex, long totalSize, DateTime lastModified, string contentType, Stream fileStream)
: base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = totalSize;
LastModified = lastModified;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = this.ContentType;
response.AddHeader(HttpWorkerRequest.GetKnownResponseHeaderName(HttpWorkerRequest.HeaderContentRange), string.Format("bytes {0}-{1}/{2}", StartIndex, EndIndex, TotalSize));
response.StatusCode = 206;
WriteFile(response);
}
protected override void WriteFile(HttpResponseBase response)
{
Stream outputStream = response.OutputStream;
using (this.FileStream)
{
byte[] buffer = new byte[0x1000];
int totalToSend = EndIndex - StartIndex;
int bytesRemaining = totalToSend;
int count = 0;
FileStream.Seek(StartIndex, SeekOrigin.Begin);
while (bytesRemaining > 0)
{
if (bytesRemaining <= buffer.Length)
count = FileStream.Read(buffer, 0, bytesRemaining);
else
count = FileStream.Read(buffer, 0, buffer.Length);
outputStream.Write(buffer, 0, count);
bytesRemaining -= count;
}
}
}
}
My MVC action:
[ByteRangeRequest("StartByte", "EndByte")]
public FileStreamResult NextSegment(int? StartByte, int? EndByte)
{
FileStream contentFileStream = System.IO.File.OpenRead(#"C:\temp\Gets.mp4");
var time = System.IO.File.GetLastWriteTime(#"C:\temp\Gets.mp4");
if (StartByte.HasValue && EndByte.HasValue)
return new ContentRangeResult(StartByte.Value, EndByte.Value, contentFileStream.Length, time, "video/x-m4v", contentFileStream);
return new ContentRangeResult(0, (int)contentFileStream.Length, contentFileStream.Length, time, "video/x-m4v", contentFileStream);
}
I really hope this helps. I spent a LOT of time on this! One thing you might want to try is removing pieces until it breaks again. It would be nice to see if the ETag stuff, modified date, etc. could be removed. I just don't have the time at the moment.
Happy coding!
I tried looking for an existing extension but I didn't immediately find one (maybe my search-fu is weak.)
My immediate thought is that you'll need to make two new classes.
First, create a class inheriting from ActionMethodSelectorAttribute. This is the same base class for HttpGet, HttpPost, etc. In this class you'll override IsValidForRequest. In that method, examine the headers to see if a range was requested. You can now use this attribute to decorate a method in your controller which will get called when someone is requested part of a stream (iOS, Silverlight, etc.)
Second, create a class inheriting from either ActionResult or maybe FileResult and override the ExecuteResult method to add the headers you identified for the byte range that you'll be returning. Return it like you would a JSON object with parameters for the byte range start, end, total size so it can generate the response headers correctly.
Take a look at the way FileContentResult is implemented to see how you access the context's HttpResponse object to alter the headers.
Take a look at HttpGet to see how it implements the check for IsValidForRequest. The source is available on CodePlex or you can use Reflector like I just did.
You might use this info to do a little more searching and see if anyone has already created this custom ActionResult already.
For reference, here is what the AcceptVerbs attribute looks like:
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
string httpMethodOverride = controllerContext.HttpContext.Request.GetHttpMethodOverride();
return this.Verbs.Contains<string>(httpMethodOverride, StringComparer.OrdinalIgnoreCase);
}
And here is what FileResult looks like. Notice the use of AddHeader:
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = this.ContentType;
if (!string.IsNullOrEmpty(this.FileDownloadName))
{
string headerValue = ContentDispositionUtil.GetHeaderValue(this.FileDownloadName);
context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
}
this.WriteFile(response);
}
I just pieced this together. I don't know if it will suit your needs (or works).
public class ContentRangeResult : FileStreamResult
{
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int TotalSize { get; set; }
public ContentRangeResult(int startIndex, int endIndex, string contentType, Stream fileStream)
:base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = endIndex - startIndex;
}
public ContentRangeResult(int startIndex, int endIndex, string contentType, string fileDownloadName, Stream fileStream)
: base(fileStream, contentType)
{
StartIndex = startIndex;
EndIndex = endIndex;
TotalSize = endIndex - startIndex;
FileDownloadName = fileDownloadName;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
if (!string.IsNullOrEmpty(this.FileDownloadName))
{
System.Net.Mime.ContentDisposition cd = new System.Net.Mime.ContentDisposition() { FileName = FileDownloadName };
context.HttpContext.Response.AddHeader("Content-Disposition", cd.ToString());
}
context.HttpContext.Response.AddHeader("Accept-Ranges", "bytes");
context.HttpContext.Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", StartIndex, EndIndex, TotalSize));
//Any other headers?
this.WriteFile(response);
}
protected override void WriteFile(HttpResponseBase response)
{
Stream outputStream = response.OutputStream;
using (this.FileStream)
{
byte[] buffer = new byte[0x1000];
int totalToSend = EndIndex - StartIndex;
int bytesRemaining = totalToSend;
int count = 0;
while (bytesRemaining > 0)
{
if (bytesRemaining <= buffer.Length)
count = FileStream.Read(buffer, 0, bytesRemaining);
else
count = FileStream.Read(buffer, 0, buffer.Length);
outputStream.Write(buffer, 0, count);
bytesRemaining -= count;
}
}
}
}
Use it like this:
return new ContentRangeResult(50, 100, "video/x-m4v", "SomeOptionalFileName", contentFileStream);
Can you move outside of MVC? This is a case where the system abstractions are shooting you in the foot, but a plain jane IHttpHandler should have alot more options.
All that said, before you implement your own streaming server, you are probably better off buying or renting one . . .
The header that work have the Content-type set to text/plain, is that correct or is a typo?.
Anyone, you can try to set this headers on the Action with:
Response.Headers.Add(...)