How Do I Make A Cache In Grails? - grails

Here it is what I'm trying or doing, I'm just displaying say around 100 tasks instance in a page. The displayed tasks are the links (click-able), clicking on it will take to that particular tasks instance show page. From that show page, the user can go back to the page he/she was before, i.e to the page of displaying 100 tasks.
Now this time again i'm fetching those 100 tasks instance from DB, I need a way to "cache" them, so that there is no need to fetch again. Is there a way to do so?
Edit:
def user = User.get(springSecurityService.principal.id)
sleep(1000)
params.max = Math.min(params.max ? params.int('max') : 10, 100)
def tasks = Tasks.findAllByIsReadAndUser(false,user,[cache:true],params)
def tasksCount = Tasks.createCriteria().list(max: params.max as Integer, offset: params.offset as Integer) {
and {
eq('user',user)
eq('isRead',false)
}
}
if(request.xhr) {
render(view:'scroll', model:[userTasks:tasks,tasksCount: tasksCount.getTotalCount()])
}
else
{
[userTasks: tasks, tasksCount: tasksCount.getTotalCount()]
}
}
Notably I have this code, which works if the user hits scroll bar with bottom of his page and fetches more data :
(function() {
jQuery(function() {
return $(window).scroll(function() {
var url;
url = $('.pagination .nextLink').attr('href');
if (url && $(window).scrollTop() > $(document).height() - $(window).height() - 50) {
$('.pagination').show();
$('.pagination').text('Fetching more data please wait...');
return $.get(url, function(data) {
$('#scrolling').append(data);
return $('.pagination').hide();
});
}
});
});
}).call(this);
Now the problem is that, even though adding cache:true in my controller code and cache is been taken place, due to this jquery code I'm again fetching the data!
How do I stop this?
Thanks in advance.

Grails supports hibernate second-level cache. You can enable it in domain object, e.g.:
class Book {
…
static mapping = {
cache true
}
}
For details take a look into Database Mapping - cache or 5.5.2.2 Caching Strategy

The cache:true parameter caches the domain objects in Hibernate's L2 cache. This is a cache between the application and the database. In other words, if you fetch the same objects from Grails again it will get the objects from memory (the L2 cache) and not query the database again.
This will not cache requests between the browser and the application. Fortunately browsers already have a cache for this purpose.

Related

Rails 5: Send reload to set of clients on data update

I'm using Rails 5 to make a simple turn based game tracker for an in-person social game (via phones/tablets/etc..)
I want to have all the 'players' in the game (list of sessions/users/...) to reload their browsers automatically once a player has taken an action.
I know that there are live update capabilities such as AJAX and websockets, but they all seem far too weighty for what seems to be a simple problem. Furthermore, I want to update other clients pages, not just the client initiating the action.
Is there a simple solution to send a reload? Or do I need to code something up in one of the more complicated APIs?
For the simple trouble, you still can use AJAX to reload user client by making interval request for each XX seconds. The server can return the last action time which can be used for client to determine that it should reload itself or not.
For example, on the controller
# SomeController
def get_last_action_time
# Return the timestamp of the last action
render json: {last_action_time: "2017-12-29 10:00:42 UTC"}
end
on the client
function getLocalLastAction () {
/* return timestamp of the last action on local */
}
function setLocalLastAction (time) {
/* Store the `time` to somewhere, ex: localStorage */
}
function checkLastAction () {
$.getJSON("/get_last_action_time", function (data) {
if (getLocalLastAction() < data.last_action_time) {
/* destroy the interval */
setLocalLastAction(data.last_action_time)
/* do the reload page */
} else {
/* do nothing */
}
})
}
// Check every 1 second, shouldn't be too short due to performance
var checking = setInterval(checkLastAction, 1000)
Then when user A do an action, the server last_action_time will change, hence client of other users will be reloaded at most after 1 second.
This way is old but quite easy to do in some simple case, and when you implement together with actions caching, the performance of app still acceptable. In the more complicated cases, I suggest using WebSocket solution for
Full control
Low latency
Better performance for app
Thanks to #yeuem1vannam's answer, here is the final code I used that helps avoid the race condition of a page loading old information while the time is being updated and then the javascript updating the time and getting the new time, and hence missing the reload.
The javascript code:
var actionChecker;
function doneChecking () {
clearInterval(actionChecker);
}
function checkLastAction () {
// Get the game ID from the html access span
var dataId = document.getElementById('javascript_data_access');
if (!dataId) return doneChecking();
var initActionTime = dataId.getAttribute('init_last_action_time');
if (!initActionTime) return doneChecking();
dataId = dataId.getAttribute('game_number');
if (!dataId) return doneChecking();
// Get the last action time
var ret = $.getJSON("/get_last_action_time/"+dataId, function (data) {
var lastActionTime = data.last_action_time;
if (!lastActionTime) return doneChecking();
if (lastActionTime>initActionTime) {
location.reload();
}
})
}
window.onload = function() {
// Check every 1 second, shouldn't be too short due to performance
actionChecker = setInterval(checkLastAction, 1000);
}
The controller's action:
def get_last_action_time
last_time = nil
begin
#game = Game.find_by_id(params[:id])
# Return the timestamp of the last action
last_time = (#game && !#game.endTime) ? #game.reloadTime.to_i : 0
rescue ActiveRecord::RecordNotFound
last_time = 0
end
# Stop bugging us after 30m, we should have moved on from this page
last_time==0 if (last_time!=0 && (milliseconds - last_time)>30*60*1000)
render json: {last_action_time: last_time}
end
And then in the html.erb:
<span id='javascript_data_access' game_number=<%= params[:id] %> init_last_action_time=<%= #game.reloadTime %>></span>
Obviously you need to add reloadTime to your model and also endTime if there's a time you no longer want to check for reloads anymore.
Seems to be working fine so far, you have to make sure that you're careful about who is in charge of setting reloadTime. If two pages set reloadTime everytime they reload, you'll be stuck in a reload loop battle between the two pages.

Autorefresh in Dashboard using ActiveAdmin

I'm developing an application in Rails wich acts as a network and apps monitor. I'm using Active Admin Dashboard as a main page, showing the status of every server and some apps in my network. I'd like to configure the dashboard page to autorefresh every x minutes, but I don't know where to configure this setting, because I don't have full control of the html rendered by the dashboard. Have anyone managed to do it?
Thanks
In config/initializers/active_admin.rb you can register javascripts:
config.register_javascript "/javascripts/admin-auto-refresh.js"
Then create a admin-auto-refresh.js that does exactly that.
You'll also want to register admin-auto-refresh.js in your config/environments/production.rb
config.assets.precompile += "admin-auto-refresh.js"
UPDATE:
Added some code to refresh the page after 5 seconds. Add this to /javascripts/admin-auto-refresh.js
$(function() {
setTimeout(location.reload(true), 5000);
})
Here's the final code, thanks very much to #JesseWolgamott.
$(function() {
var sPath = window.location.pathname;
var sPage = sPath.substring(sPath.lastIndexOf('/') + 1);
if (sPage == 'admin'){
setTimeout("location.reload(true);", 10000);
}
})
Below is code that will refresh without the page flashing. It is enabled by having at least one element in the page with a class tag of needs_updating. Include this code snippet in any javascript that is loaded and then add the tag any where on the page.
The only downside is this ONLY updates the html body of the page.
For example
show do |my_model|
...
if my_model.processing?
row :status, class: 'needs_updating' do
'we are working on it...'
end
else
row :status do
'ready'
end
end
....
end
so if the model is still processing then you get the class tag 'needs_updating' which will cause the below javascript to be invoked every 10 seconds
jQuery(document).ready(function($) {
if ($('.needs_updating').length > 0) {
console.log("we need some updating soon");
var timer = setTimeout(function () {
console.log("re-loading now");
$.ajax({
url: "",
context: document.body,
success: function(s,x) {
$(this).html(s);
if ($('.needs_updating').length == 0) {
clearInterval(timer);
}
}
});
}, 10000)
}
})

ehcache is not honoring maxElementsInMemory

I have a fairly simple cache configuration:
<cache name="MyCache"
maxElementsInMemory="200000"
eternal="false"
timeToIdleSeconds="43200"
timeToLiveSeconds="43200"
overflowToDisk="false"
diskPersistent="false"
memoryStoreEvictionPolicy="LRU"
/>
I create my cache in the following way:
private Ehcache myCache =
CacheManager.getInstance().getEhcache("MyCache");
I use my cache like this:
public MyResponse processRequest(MyRequest request) {
Element element = myCache.get(request);
if (element != null) {
return (MyResponse)element.getValue();
} else {
MyResponse response = remoteService.process(request);
myCache.put(new Element(request, response));
return response;
}
}
Every 10,000 calls to processRequest() method, I log stats about my cache like this:
logger.debug("Cache name: " + myCache.getName());
logger.debug("Max elements in memory: " + myCache.getMaxElementsInMemory());
logger.debug("Memory store size: " + myCache.getMemoryStoreSize());
logger.debug("Hit count: " + myCache.getHitCount());
logger.debug("Miss count: " + myCache.getMissCountNotFound());
logger.debug("Miss count (because expired): " + myCache.getMissCountExpired());
..I see a good amount of hits, which tells me that it's working.
..However, what I'm seeing is that after a couple hours, the getMemoryStoreSize() is starting to exceed getMaxElementsInMemory(). Eventually, it gets bigger and bigger, and renders the jvm unstable because GC is starting to do Full GCs nonstop to reclaim memory (and I have a pretty large cap set). When I profiled the heap, it pointed to the LRU's SpoolingLinkedHashMap taking most of the space.
I do have a lot of requests hitting this cache, and my theory is that ehcache's LRU algorithm is perhaps not keeping up with evicting the elements when it's full. I tried LFU policy and it also caused the memory store to go over maxElements.
I then started looked at the ehcache code to see if I could prove my theory (inside LruMemoryStore$SpoolingLinkedHashMap):
private boolean removeLeastRecentlyUsedElement(Element element) throws CacheException {
//check for expiry and remove before going to the trouble of spooling it
if (element.isExpired()) {
notifyExpiry(element);
return true;
}
if (isFull()) {
evict(element);
return true;
} else {
return false;
}
}
..from here looks ok, then looked at the evict() method:
protected final void evict(Element element) throws CacheException {
boolean spooled = false;
if (cache.isOverflowToDisk()) {
if (!element.isSerializable()) {
if (LOG.isDebugEnabled()) {
LOG.debug(new StringBuffer("Object with key ").append(element.getObjectKey())
.append(" is not Serializable and cannot be overflowed to disk"));
}
} else {
spoolToDisk(element);
spooled = true;
}
}
if (!spooled) {
cache.getCacheEventNotificationService().notifyElementEvicted(element, false);
}
}
..this looks like it doesn't actually evict (despite the name) but rather relies on the caller to evict. So I looked at the implementation of the put() method and I don't see it calling it. I'm clearly missing something here and would appreciate some help on this.
Thanks!
Your configuration looks fine to me. Only need is to use right key for caching.
Do not put complete request object as your key for cache. Put some unique value from your request object. For example:
MyResponse response = remoteService.process(request);
myCache.put(new Element(request.getCustomerID(), response));
return response;
This should work for you. The reason your caching is not working is that each time your request object is new object; it never finds the response from cache, so it keeps adding into cache.
maxElementsInMemory attribute is deprecated, use maxEntriesLocalHeap instead

Persisting jqGrid column preferences

I've got a few jqGrids on my ASP.NET MVC 3 application that have a number of columns. I added the following to the column definitions to default some columns to be hidden:
colModel: [
{ name: 'IceCreamID', hidden: true},
{ name: 'RecipeID', hidden: true }
and this works nicely. Those columns aren't visible on my grid.
Then I added this to implement the column chooser:
var grid = $('#icecreamGrid');
grid.jqGrid('navButtonAdd', '#icecreamPager',
{ caption: "Columns", buttonicon: "ui-icon-calculator",
title: "Choose Columns",
onClickButton: function() {
grid.jqGrid('columnChooser');
}
});
Great, brings up the column chooser now. I then added the following to columns I never wanted to show up in the column chooser:
colModel: [
{ name: 'IceCreamID', hidden: true, hidedlg: true},
So I can now hide/show columns just fine. Now, how would you persist this information? DB? As a cookie? Other way? Is there a preferred way to store this sort of information that is really a user preference rather than something related to the data itself?
More Info
Based on Oleg's comment below, I want to provide a little more information.
The point here is that I've got grids with 10-15 columns which could be display based on the user's preference. For a simple example, one of my grid's has the following 9 columns:
IceCream|ShortName|HasNuts|SugarAdded|LimitedRun|PromoItem|Facility|FirstRun|LastRun
Users can hide/show any of these 9 columns based on their personal preferences.
What I want to do is provide a way to persist which columns a particular user wants to see so that s/he doesn't have to re-choose those columns to view each time the page with that grid is shown.
I found you question very interesting. The question about saving the user state of grid are interesting in many cases. There are some interesting answers on such problems which uses cookie (see here for example).
In my opinion saving of the grid state in database on the server or in the localStorage is better way as the usage of cookie. The best way depends on the project's requirements in which you use it. For example the usage of the database storage on the server allows you to implement roaming state of the grid. If you use the localStorage instead of cookies the user preferences will be lost if the user goes to another computer or just if the user will use another web browser on the same computer.
Another problem with the grid state is the maintenance. The information about the columns of the grid you hold typically in the JavaScript or HTML files and not in the database. In the case the both sources can be not synchronous on the changes in the grid. Different scenarios of the update problem could you easy imagine. Nevertheless the advantages of user's preferences so large in some scenarios that the problems with small disadvantages are not so important and can be solved relatively easy.
So I'll spend some time to implement two demos which shows how it can be implemented. I used localStorage in my demos because of many reasons. I mention only two from there:
Cookies is the way which send permanently different information to or from the server which is not really requited. It increases the size of HTTP header and decreases the performance of the web site (see here for example).
Cookies have very hard restrictions. Corresponds to the section 6.3 of rfc2109 or 6.1 of rfc6265: At least 4096 bytes per cookie, at least 50 cookies per domain (20 in rfc2109), at least 3000 cookies total (300 in rfc2109). So the cookies one can't use to save too many information. For example if you would save state of every grid of every your web page you can quickly achieve the limits.
On the other side localStorage are supported by all modern browsers and will be supported in Internet Explorer starting with IE8 (see here). The localStorage will be automatically saved per origins (like a1.example.com, a2.example.com, a3.example.com, etc) and has arbitrary limit of 5 MB per origin (see here). So if you use the space carefully you will far from the any limits.
So I used in my demos the localStorage. I should additionally mention that there are some plugins like jStorage which use localStorage if it's supported by the browser and use another storage, but the same interface for you in case of old browsers like IE6/IE7. In the case you has only less size of storage: 128 kB instead of 5 MB, but it's better as 4K which one has for cookies (see here).
Now about the implementation. I creates two demos: this and it's extended version: this.
In the first demo the following states of grid will be saved and automatically restored on the page reload (F5 in the most web browsers):
which column are hidden
the order of columns
the width of every column
the name of the column by which the grid will be sorted and the sort direction
the current page number
the current filter of the grid and the flag whether the filter are applied. I used multipleSearch: true setting in the grid.
In the same way one can extend (or reduce) the list of options which are the part of the saved grid state.
The most important parts of the code from the demo you will find below:
var $grid = $("#list"),
saveObjectInLocalStorage = function (storageItemName, object) {
if (typeof window.localStorage !== 'undefined') {
window.localStorage.setItem(storageItemName, JSON.stringify(object));
}
},
removeObjectFromLocalStorage = function (storageItemName) {
if (typeof window.localStorage !== 'undefined') {
window.localStorage.removeItem(storageItemName);
}
},
getObjectFromLocalStorage = function (storageItemName) {
if (typeof window.localStorage !== 'undefined') {
return $.parseJSON(window.localStorage.getItem(storageItemName));
}
},
myColumnStateName = 'ColumnChooserAndLocalStorage.colState',
saveColumnState = function (perm) {
var colModel = this.jqGrid('getGridParam', 'colModel'), i, l = colModel.length, colItem, cmName,
postData = this.jqGrid('getGridParam', 'postData'),
columnsState = {
search: this.jqGrid('getGridParam', 'search'),
page: this.jqGrid('getGridParam', 'page'),
sortname: this.jqGrid('getGridParam', 'sortname'),
sortorder: this.jqGrid('getGridParam', 'sortorder'),
permutation: perm,
colStates: {}
},
colStates = columnsState.colStates;
if (typeof (postData.filters) !== 'undefined') {
columnsState.filters = postData.filters;
}
for (i = 0; i < l; i++) {
colItem = colModel[i];
cmName = colItem.name;
if (cmName !== 'rn' && cmName !== 'cb' && cmName !== 'subgrid') {
colStates[cmName] = {
width: colItem.width,
hidden: colItem.hidden
};
}
}
saveObjectInLocalStorage(myColumnStateName, columnsState);
},
myColumnsState,
isColState,
restoreColumnState = function (colModel) {
var colItem, i, l = colModel.length, colStates, cmName,
columnsState = getObjectFromLocalStorage(myColumnStateName);
if (columnsState) {
colStates = columnsState.colStates;
for (i = 0; i < l; i++) {
colItem = colModel[i];
cmName = colItem.name;
if (cmName !== 'rn' && cmName !== 'cb' && cmName !== 'subgrid') {
colModel[i] = $.extend(true, {}, colModel[i], colStates[cmName]);
}
}
}
return columnsState;
},
firstLoad = true;
myColumnsState = restoreColumnState(cm);
isColState = typeof (myColumnsState) !== 'undefined' && myColumnsState !== null;
$grid.jqGrid({
// ... other options
page: isColState ? myColumnsState.page : 1,
search: isColState ? myColumnsState.search : false,
postData: isColState ? { filters: myColumnsState.filters } : {},
sortname: isColState ? myColumnsState.sortname : 'invdate',
sortorder: isColState ? myColumnsState.sortorder : 'desc',
loadComplete: function () {
if (firstLoad) {
firstLoad = false;
if (isColState) {
$(this).jqGrid("remapColumns", myColumnsState.permutation, true);
}
}
saveColumnState.call($(this), this.p.remapColumns);
}
});
$grid.jqGrid('navButtonAdd', '#pager', {
caption: "",
buttonicon: "ui-icon-calculator",
title: "choose columns",
onClickButton: function () {
$(this).jqGrid('columnChooser', {
done: function (perm) {
if (perm) {
this.jqGrid("remapColumns", perm, true);
saveColumnState.call(this, perm);
}
}
});
}
});
$grid.jqGrid('navButtonAdd', '#pager', {
caption: "",
buttonicon: "ui-icon-closethick",
title: "clear saved grid's settings",
onClickButton: function () {
removeObjectFromLocalStorage(myColumnStateName);
}
});
Be carefully to define myColumnStateName (the value `'ColumnChooserAndLocalStorage.colState'``) in the demo) to different values on the different pages.
The second demo is the extension of the first one using the technique from my old answer to your another question. The demo use the searching toolbar and synchronize additionally information between the advanced searching form and the searching toolbar.
UPDATED: The next answer contains extended version of the code included above. It shows how to persist the selected rows (or row) additionally. Another answer shows how to persist the list of expanded nodes of the tree grid and expand the nodes on the relaoding of the page.

ASP.NET MVC ajax chat

I built an ajax chat in one of my mvc website. everything is working fine. I am using polling. At certain interval i am using $.post to get the messages from the db. But there is a problem. The message retrieved using $.post keeps on repeating. here is my javascript code and controller method.
var t;
function GetMessages() {
var LastMsgRec = $("#hdnLastMsgRec").val();
var RoomId = $("#hdnRoomId").val();
//Get all the messages associated with this roomId
$.post("/Chat/GetMessages", { roomId: RoomId, lastRecMsg: LastMsgRec }, function(Data) {
if (Data.Messages.length != 0) {
$("#messagesCont").append(Data.Messages);
if (Data.newUser.length != 0)
$("#usersUl").append(Data.newUser);
$("#messagesCont").attr({ scrollTop: $("#messagesCont").attr("scrollHeight") - $('#messagesCont').height() });
$("#userListCont").attr({ scrollTop: $("#userListCont").attr("scrollHeight") - $('#userListCont').height() });
}
else {
}
$("#hdnLastMsgRec").val(Data.LastMsgRec);
}, "json");
t = setTimeout("GetMessages()", 3000);
}
and here is my controller method to get the data:
public JsonResult GetMessages(int roomId,DateTime lastRecMsg)
{
StringBuilder messagesSb = new StringBuilder();
StringBuilder newUserSb = new StringBuilder();
List<Message> msgs = (dc.Messages).Where(m => m.RoomID == roomId && m.TimeStamp > lastRecMsg).ToList();
if (msgs.Count == 0)
{
return Json(new { Messages = "", LastMsgRec = System.DateTime.Now.ToString() });
}
foreach (Message item in msgs)
{
messagesSb.Append(string.Format(messageTemplate,item.User.Username,item.Text));
if (item.Text == "Just logged in!")
newUserSb.Append(string.Format(newUserTemplate,item.User.Username));
}
return Json(new {Messages = messagesSb.ToString(),LastMsgRec = System.DateTime.Now.ToString(),newUser = newUserSb.ToString().Length == 0 ?"":newUserSb.ToString()});
}
Everything is working absloutely perfect. But i some messages getting repeated. The first time page loads i am retrieving the data and call GetMessages() function. I am loading the value of field hdnLastMsgRec the first time page loads and after the value for this field are set by the javascript.
I think the message keeps on repeating because of asynchronous calls. I don't know, may be you guys can help me solve this.
or you can suggest better way to implement this.
Kaivalya is correct about the caching, but I'd also suggest that your design could and should be altered just a tad.
I made a very similar app recently, and what I found was that my design was greatly enhanced by letting the controllers work with the fairly standard PRG pattern (post-redirect-get). Why enhanced? well, because POST methods are built to add stuff to an app, GET methods are supposed to be used to get information without side effects. Your polling should be just getting new messages w/o side effects.
So rather than your $.post call expecting data and handling the callback, what I'd recommend is having your controller expose a method for creating new chat messages via POST and then another method that get the last X chat messages, or the messages since a certain timestamp or whatever.
The javascript callback from the post action, then can update some variables (e.g. the last message id, timestamp of the last message, or even the whole URL of the next message based on the info contained in a redirect, whatever).
The $.post would fire only in response to user input (e..g type in a box, hit 'send') Then, you have (separately) a $.get call from jquery that's set up to poll like you said, and all it does is fetch the latest chat messages and it's callback updates the chat UI with them.
I got my answer here: ASP.NET AJAX CHAT
The names below i am referring to are from above link.
i think the actual problem was with the timestamp thing and asynchronous behaviour of $.post. after calling "GetMessages()" method, even if the previous request to retrive chat message was not complete anathor call to same method used to fire due to setting timeout for "GetMessages()" method outside the $.post method. In my question you can see that timeout for "GetMessages()" method is set outside the $.post method. Now i set the timeout for "GetMessages()" method inside the $.post method. so that next call to "GetMessages()" only occur after 3 seconds of completion of current $.post method. I have posted the code below.
var t;
function GetMessages() {
var LastMsgRec = $("#hdnLastMsgRec").val();
var RoomId = $("#hdnRoomId").val();
//Get all the messages associated with this roomId
$.post("/Chat/GetMessages", { roomId: RoomId, lastRecMsg: LastMsgRec }, function(Data) {
if (Data.LastMsgRec.length != 0)
$("#hdnLastMsgRec").val(Data.LastMsgRec);
if (Data.Messages.length != 0) {
$("#messagesCont").append(Data.Messages);
if (Data.newUser.length != 0)
$("#usersUl").append(Data.newUser);
$("#messagesCont").attr({ scrollTop: $("#messagesCont").attr("scrollHeight") - $('#messagesCont').height() });
$("#userListCont").attr({ scrollTop: $("#userListCont").attr("scrollHeight") - $('#userListCont').height() });
}
else {
}
t = setTimeout("GetMessages()", 3000);
}, "json");
}
I addition to that i also changed few things. As suggested by ignatandrei i placed $("#hdnLastMsgRec").val(Data.LastMsgRec); immediately after function(Data) {.
and also
as said by MikeSW i changed the data retrieval process. Previously i was extracting data on the basis of timespan(retrieve all the data associated with
this room id that has greater timespan than last data retrieved message timespan) but now i keep track of the messageid. Now i retrieve only those data that
has message id greater than last retrieved message id.
and guess what no repeataion and perfectly working chat application so far on my intranet.
I still got to see it's performance when deployed on internet.
i think it solved my problem.
i will still test the system and let u guys know if there is any problem.
By default $.post() caches the results
You can either call $.ajaxSetup ({ cache: false}); before JS GetMessages function call to ensure caching is disabled or change the $.post to $.ajax and set cache attribute to false. In the end $.post() is a short cut to this:
$.ajax({
type: 'POST',
url: url,
data: data,
success: success
dataType: dataType
});

Resources