Friday, 18 July 2008

Quartz Framework

Just now i completed understanding quartz and practicing few examples.

I wanted to share few info with my readers.

1) Quartz is a full-featured, open source job scheduling system that can be integrated with, or used along side virtually any J2EE or J2SE application - from the smallest stand-alone application to the largest e-commerce system. Quartz can be used to create simple or complex schedules for executing tens, hundreds, or even tens-of-thousands of jobs; jobs whose tasks are defined as standard Java components or EJBs. The Quartz Scheduler includes many enterprise-class features, such as JTA transactions and clustering.

2) For Developing Hello World Application using quartz please follow this
link


public class HelloJob implements Job {

public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException{
System.out.println("Hello World Quartz Scheduler: " + new Date());
}
}

public class HelloSchedule {

public HelloSchedule()throws Exception{
SchedulerFactory sf=new StdSchedulerFactory();
Scheduler sched=sf.getScheduler();
sched.start();

JobDetail jd=new JobDetail("myjob",sched.DEFAULT_GROUP,HelloJob.class);

SimpleTrigger st=new SimpleTrigger("mytrigger",sched.DEFAULT_GROUP,new Date(),
null,SimpleTrigger.REPEAT_INDEFINITELY,60L*1000L);

sched.scheduleJob(jd, st);
}

public static void main(String args[]){
try{
new HelloSchedule();
}catch(Exception e){}
}
}



Description of the code

execute(): Any software components you want to schedule then you must implement the Job interface and override it execute() method.

JobExecutionContext: The JobExecutionContext object that is passed to execute() method provides the job instance with information about its "run-time" environment - a handle to the Scheduler that executed it, a handle to the Trigger that triggered the execution, the job's JobDetail object, and a few other items.

SchedulerFactory: SchedulerFactory is a interface provides a mechanism for obtaining client-usable handles to Scheduler instances.

StdSchedulerFactory(): A Class StdSchedulerFactory is a class and it is implementation of SchedulerFactory interface. Here it just using for create an instance of SchedulerFactory instance.

Scheduler: Scheduler interface is the main interface (API) to this functionality. It provides some simple operations like scheduling jobs, unscheduling jobs, starting/stopping/pausing the scheduler.

getScheduler(): SchedulerFactoy interface having the getScheduler() method that returns an instance of Scheduler.

start(): This method is used to starts the Scheduler's threads that fire Triggers. At the first time when we create the Scheduler it is in "stand-by" mode, and will not fire triggers. The scheduler can also be send back into stand-by mode by invoking the standby() method.

JobDetail(String name, String group, Class jobclass): The JobDetail object is created at the time the Job is added to scheduler. It contains various property settings like job name, group name and job class name. It can be used to store state information for a given instance of job class.

SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval): Trigger objects are used to firing the execution of jobs. When you want to schedule the job, instantiate the trigger and set the properties to provide the scheduling.

DEFAULT_GROUP: It is a constant, specified that Job and Trigger instances are belongs to which group..

REPEAT_INDEFINITELY: It is a constant used to indicate the 'repeat count' of the trigger is indefinite.

scheduleJob(JobDetail jd, SimpleTrigger st): This method is used to add the JobDetail to the Scheduler, and associate the Trigger with it.

3) A Job instance can be defined as Stateful or Non-Stateful. Non-Stateful jobs only have their JobDataMap and it stored at the time when the jobs are added to the scheduler. This means during execution of the job any changes are made to the job data map will be lost and a Stateful job is just opposite - its JobDataMap is re-stored after every execution of the job. One disadvantage of Stateful job is that it cannot be executed concurrently. Or in other words: In Stateful job, and a trigger attempts to 'fire' the job while it is already executing, the trigger will block (wait) until the previous execution completes. You can specify a job as Stateful by implementing the StatefulJob interface instead of Job interface.

4) There are following examples for implementing the SimpleTrigger:

a). Example SimpleTrigger : Create a simple trigger which fires exactly once, 20 seconds from now:


SimpleTrigger strigger = new SimpleTrigger("mySimpleTrigger", sched.DEFAULT_GROUP, new Date(startTime), null, 0, 0L);


b). Example SimpleTrigger : Create a simple trigger that fires quickly and repeats every 20 seconds:


SimpleTrigger strigger = new SimpleTrigger("mySimpleTrigger", sched.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 20L * 1000L);


c). Example SimpleTrigger: Create a Simple Trigger that fires quickly and repeats every 10 seconds until 50 seconds from now:


long endTime = System.currentTimeMillis() + (50L * 1000L);

SimpleTrigger strigger = new SimpleTrigger("mySimpleTrigger", sched.DEFAULT_GROUP, new Date(), new Date(endTime), SimpleTrigger.REPEAT_INDEFINITELY, 10L * 1000L);


d). Example SimpleTrigger: Create a Simple Trigger that fires on February 19 of the year 2007 at accurately 9:15 am, and repeats 10 times with 20 seconds delay between each firing.


java.util.Calendar cal = new java.util.GregorianCalendar(2007,cal.FEB, 19);
cal.set(cal.HOUR, 9);
cal.set(cal.MINUTE, 15);
cal.set(cal.SECOND, 0);
cal.set(cal.MILLISECOND, 0);

Data startTime = cal.getTime();

SimpleTrigger trigger = new SimpleTrigger("mySimpleTrigger", sched.DEFAULT_GROUP, startTime, 10, 20L*1000L);


5) CronTriggers are more useful than the SimpleTrigger, if we want to performed the job triggering based on the calendar schedules such as "every day", "every weekday" etc. This is also useful when we need to fire jobs in a schedule that is based on the calendar schedule on the exact specified time intervals of SimpleTrigger. Here we will execute an expression that fires at 8:30, 9:30, 10:30, and 11:30 on every Monday and Saturday.

Cron Expression

The Cron-Expressions are strings which are used for configuring the instances of CronTrigger. The Cron-Expressions made up of following sub-expressions that performs individual works according to it's schedule and that is separated by the white-space. :

Seconds
Minutes
Hours
Day-of-Month
Month
Day-of-Week

Example: The Cron-expression string is "0 0 10 ? * SUN" that means "every Sunday at 10 am". This example reads only the "SUN" from weekday and replaces to all weekday.

The Wild-cards ('* ' character) that can be used for inserting the every possible value of this field. The '*' character is used in the "Month" field that means "every month" and "Day-Of-Week" field means "every day of the week".

All fields have some specific values that are specified by us. Such as the numbers 0 to 23 for hours, 0 to 59 that is used for minutes and seconds, 0 to 31 for Day-of-Month but here, we should more careful about how many day are used in a month. Months have the specified values between 0 to 11, for this we will use the some string as like: JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC. Similarly, the Days-of-week has specified values between 1 to 7. Here 1 for Sunday, 2 for Monday, 3 for Tuesday,....... so on. But here we will use the some string for this like: SUN, MON, TUE, WED, THU, FRI and SAT.

The Cron-Expressions are used the '/' character for increments to specify values. If we will put "0/10" in the seconds field that means every 10 seconds will start at second zero and if we will put "0/20" in the Minutes field then it will simply fires every 20 minutes.

The Cron-Expressions are also used the '?' character that allows for the day-of-month and day-of-week fields for specifying "no specific value".

The 'L' character, it is the short form of "last" that allows us for using the day-of -month and day-of-week fields. If we can use the value 'L' in the day-of-month field that means last day of the month like: 31 for January, 28 for February not the leap years. In case of day-of-week field that means "7 stands for SAT".

There are following example of expressions for specify the JavaDOC for CronTrigger:

a.Example: Write an expression to create a trigger that fires ever 10 minutes.

"0 0/10 * * * ?"

b.Example: Write an expression to create a trigger that fires every 10 minutes, at 10 seconds after the minute.

"10 0/10 * * * ?"

(That means each firing after the 10 seconds interval like: 8:00:00am, 8:10:00am,8:20:00am etc.)

c.Example: Write an expression to create a trigger that fires at 9:30, 10:30, 11:30, 12:30 and 13:30 on every Sunday and Saturday.

"0 30 9-13 ? * SUN, SAT"


6) I think this article is also fine.Few points i want to cover from this article are as follows

a) Quartz is an open source job scheduling framework that provides simple but powerful mechanisms for job scheduling in Java applications. Quartz allows developers to schedule jobs by time interval or by time of day. It implements many-to-many relationships for jobs and triggers and can associate multiple jobs with different triggers. Applications that incorporate Quartz can reuse jobs from different events and also group multiple jobs for a single event. While you can configure Quartz through a property file (in which you can specify a data source for JDBC transactions, global job and/or trigger listeners, plug-ins, thread pools, and more) it is not at all integrated with the application server's context or references. One result of this is that jobs do not have access to the Web server's internal functions; in the case of the WebSphere application server, for example, Quartz-scheduled jobs cannot interfere with the server's Dyna-cache and data sources.

b) A CronTrigger allows for more specific scheduling than a SimpleTrigger and is still not very complex. Based on cron expressions, CronTriggers allow for calendar-like repeat intervals rather than uniform repeat intervals -- a major improvement over SimpleTriggers.

Cron expressions consist of the following seven fields:

Seconds
Minutes
Hours
Day-of-month
Month
Day-of-week
Year (optional field)
Special characters

Cron triggers utilize a series of special characters, as follows:

The backslash (/) character denotes value increments. For example, "5/15" in the seconds field means every 15 seconds starting at the fifth second.

The question (?) character and the letter-L (L) character are permitted only in the day-of-month and day-of-week fields. The question character indicates that the field should hold no specific value. Therefore, if you specify the day-of-month, you can insert a "?" in the day-of-week field to indicate that the day-of-week value is inconsequential. The letter-L character is short for last. Placed in the day-of-month field, this schedules execution for the last day of the month. In the day-of-week field, an "L" is equivalent to a "7" if placed by itself or means the last instance of the day-of-week in the month. So "0L" would schedule execution for the last Sunday of the month.


The letter-W (W) character in the day-of-month field schedules execution on the weekday nearest to the value specified. Placing "1W" in the day-of month field schedules execution for the weekday nearest the first of the month.


The pound (#) character specifies a particular instance of a weekday for a given month. Placing "MON#2" in the day-of-week field schedules a task on the second Monday of the month.


The asterisk (*) character is a wildcard character and indicates that every possible value can be taken for that specific field

The CronTrigger is based on Calendar-like schedules. When you need a Job executed every day at 10:30 a.m., except on Saturdays and Sundays, then a CronTrigger should be used. As the name implies, the CronTrigger is based on the Unix cron expression. As an example, the following Quartz cron expression would execute a Job at 10:15 a.m. every day, Monday through Friday.

0 15 10 ? * MON-FRI

and this expression

0 15 10 ? * 6L 2002-2005
fires at 10:15 a.m. on the last Friday of every month during the years 2002, 2003, 2004, and 2005


7) Even this article also introduces in good manner about quartz.

8) Below are supported runtime environments.

Quartz can run embedded within another free standing application

Quartz can be instantiated within an application server (or servlet container), and participate in XA transactions

Quartz can run as a stand-alone program (within its own Java Virtual Machine), to be used via RMI

Quartz can be instantiated as a cluster of stand-alone programs (with load-balance and fail-over capabilities)

9) Why not just use java.util.Timer?

There are many reasons! Here are a few:

Timers have no persistence mechanism.

Timers have inflexible scheduling (only able to set start-time & repeat interval, nothing based on dates, time of day, etc.)

Timers don't utilize a thread-pool (one thread per timer)

Timers have no real management schemes - you'd have to write your own mechanism for being able to remember, organize and retreive your tasks by name, etc.

10) Listeners can be particularly useful when you want a notification if something goes wrong in the system. For example, if an error occurs during report generation, an elegant way to notify the development team about it is to make a JobListener that will send an email or SMS.

A JobListener can provide more interesting functionality. Imagine a Job that has to deal with a task that is highly dependent on some system resource availability (such as a network that is not stable enough). In this case, you can make a listener that will re-trigger that job if the resource is not available when the job is executed.

Trigger implementations (such as CronTrigger) can define new types of misfire instructions that can be useful. You should check out the Javadocs for these classes for more information on this topic. Using the TriggerListener, you can gain more control on actions that should be used if a misfire occurs. Also, you can use it to react to trigger events, such as a trigger's firing and completion.

SchedulerListener deals with global system events, such as scheduler shutdown or the addition or removal of jobs and triggers.

Here, we will just demonstrate a simple JobListener for our report generation example. First we have to write a class to implement the JobListener interface


public class MyJobFailedListener implements JobListener {

public String getName() {
return "FAILED JOB";
}

public void jobToBeExecuted(JobExecutionContext arg0) {
}

public void jobExecutionVetoed(JobExecutionContext context) {

}

public void jobWasExecuted(JobExecutionContext context,JobExecutionException exception) {

if (exception != null) {
System.out.println("Report generation error");
// TODO notify development team
}
}
}



and then add the following line to the main method of our example:

sched.addGlobalJobListener(new MyJobFailedListener());
By adding this listener to the global list of scheduler job listeners, it will be called for all of the jobs. Of course, there is a way to set listeners only for a particular job. To do this, you should use Scheduler's addJobListeners() method to register the listener with the scheduler. Then add the registered listener to the job's list of listeners with JobDetail's addJobListener() method, with the listener name as a parameter (the value returned from getName() method of the listener).

sched.addJobListener(new MyJobFailedListener());
jobDetail.addJobListener("FAILED JOB");

To test if this listener really works, simply put

throw new JobExecutionException();

11) I will suggest this article for learning Scheduling Jobs in a Java Web Application.

Example declaration gives one way to achieve this.

web.xml

<listener>
<listener-class>org.quartz.ee.servlet.QuartzInitializerListener</listener-class>
</listener>

<listener>
<listener-class>HelloWorldServletListener</listener-class>
</listener>



HelloJob.java

public class HelloJob implements Job {

public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException{
System.out.println("Hello World listener------>: " + new Date());
}
}


HelloWorldServletListener.java

public class HelloWorldServletListener implements ServletContextListener{

public static final String QUARTZ_FACTORY_KEY = "org.quartz.impl.StdSchedulerFactory.KEY";

public void contextInitialized(ServletContextEvent sce) {

System.out.println("Entered into contextInitlialized....................>>>>>>>");
String factoryKey =
sce.getServletContext().getInitParameter("servlet-context-factory-key");
if (factoryKey == null) {
factoryKey = QUARTZ_FACTORY_KEY;
}

StdSchedulerFactory sf = (StdSchedulerFactory) sce.getServletContext().getAttribute(factoryKey);
try {
Scheduler sched=sf.getScheduler();
JobDetail jd=new JobDetail("myjob",sched.DEFAULT_GROUP,HelloJob.class);

SimpleTrigger st=new SimpleTrigger("mytrigger",sched.DEFAULT_GROUP,new Date(),
null,SimpleTrigger.REPEAT_INDEFINITELY,60L*1000L);

sched.scheduleJob(jd, st);

} catch (Exception ex) {
}
}

public void contextDestroyed(ServletContextEvent sce) {

}
}


12) Please do read this article also to get more understanding about Job Scheduling in Java.

13) Here is a list of just a few of the hundreds of Quartz users:

Vodafone Ireland - uses Quartz for scheduling tests to be carried out on systems in order to generate quality of service information.

Covalent Technologies, Inc. - uses Quartz within their CAM product to handle anything scheduling related in the system, such as: scheduling server actions (start, restart, etc.), metric calculations, data cleanup daemons, etc.

PartNET Inc. - uses Quartz for scheduling application events and driving workflows within many of its products.

U.S. Department of Defence - uses Quartz at various points within a large electronic commerce application, noteably order fulfillment.

Level3 Communications - uses Quartz to drive software builds and deployments.

Atlassian - uses Quartz within their excellent JIRA and Confluence products.

Cisco - uses Quartz in various in-house systems.

Apache Jakarta - Quartz is used within (or as plugins to) several products falling under the Jakarta umbrella.

OpenSymphony - Uses Quartz to drive the OS Workflow product.

Spring - Quartz is used within the Spring Framework.

XpoLog - uses Quartz within XpoLog Center in order to enable automatic technical support.

Bloombase Technologies - has integrating Quartz within their Spitfire EAI Server for XML security processing.

Thomson Tax and Accounting - uses Quartz in its job scheduling framework within it's editorial systems group.

The Liferay Portal - is using the Quartz Scheduler. Just download and check the code. Liferay Inc. Portal.

Infoglue CMS - from infoglue.org

Apache Cocoon - uses Quartz to provide scheduling features and to run application background processes.

JBoss - uses Quartz for the implementation of a number of services within its infrastructure.

14) Wonderful tutorial on cron triggers is available at this official site.

15) We can find complete tutorial on this official quartz site.

16) Finally i suggest reading Quartz cookbook available at quartz official web site.

Hope this explanation helps.

38 comments:

Unknown said...

A Very good article on quartz, takes very less time to understand the whole concept of quartz.

JP said...

Ratnakar,

Thanks a lot for your comments.

Thanks
Prashant

Girish said...

Hi Prashant,

This Blog is really having good information regarding quartz.

Can you please suggest me regarding

What is difference between following expressions.

0 0/10 * * * ?
and
0 0/10 * ? * *

Whether both the statements will execute the job for every 10 minutes.
or is there any difference.

Basically I am confused regarding use of ? and *
If you could detail the same.
That would be very nice.

Once again thank you for sharing such a nice information.

-
Girish

Naresh Bahety said...

HI ,
Nice post but i want to know about limitations of Quartz ,please help

Thanks in advance,
Naresh.

Anonymous said...

Hi i am kavin, its my first time to commenting
anyplace, when i read this piece of writing i thought i could also create comment due to this good piece of writing.
Here is my webpage :: private krankenversicherung tarif

Anonymous said...

Excellent post. I certainly love this website. Keep writing!
Here is my weblog ; hauskauf trotz schufa

Anonymous said...

If you want to grow your familiarity just keep visiting
this web site and be updated with the hottest news posted here.
My homepage ; krankenversicherung selbständige vergleich

Anonymous said...

Hi my family member! I wish to say that this article is
awesome, nice written and come with almost all
important infos. I would like to peer extra posts like this .


Look into my page: private krankenversicherung
Also visit my web-site ...

Anonymous said...

This information is priceless. How can I find out more?


Here is my web site ... gesetzliche krankenversicherung vergleich stiftung warentest
Review my web blog

Anonymous said...

Heya i'm for the primary time here. I found this board and I in finding It truly useful & it helped me out a lot. I am hoping to provide one thing back and help others like you aided me.
Check out my blog - Billige Kleidung online Kaufen

Anonymous said...

Thanks for your marvelous posting! I quite enjoyed reading
it, you're a great author. I will make certain to bookmark your blog and will come back someday. I want to encourage you to definitely continue your great writing, have a nice day!
Also visit my homepage - google seo companies

Anonymous said...

I all the time emailed this web site post page
to all my associates, because if like to read it afterward my
friends will too.
My weblog :: affiliate products to promote

Anonymous said...

Great article.
Feel free to visit my site ; pkv basistarif vergleich

Anonymous said...

What's up to every body, it's my first go to see of this web
site; this blog contains remarkable and in fact good stuff in support of readers.
Also visit my web-site : Страница не найдена

Anonymous said...

Hi, every time i used to check webpage posts here early in the dawn, since i like to learn more and more.
Also visit my web-site : work At home Business opportunity

Anonymous said...

I'm extremely pleased to discover this website. I want to to thank you for ones time just for this wonderful read!! I definitely loved every bit of it and I have you saved to fav to look at new things in your web site.
Feel free to visit my web-site ; refinance with bad credit score

Anonymous said...

Hmm is anyone else encountering problems with the images on this blog loading?
I'm trying to figure out if its a problem on my end or if it's the blog.
Any responses would be greatly appreciated.
Here is my web site : krankenversicherung gesetzlich vergleich

Anonymous said...

I got this web page from my buddy who told me concerning this site and now this time I am browsing this web site and reading very informative articles or reviews at this place.
Also visit my web-site ; bad credit home mortgage loan

Anonymous said...

What's up mates, how is everything, and what you desire to say on the topic of this paragraph, in my view its in fact awesome designed for me.

Here is my web-site - vertrag trotz negativer schufa

Anonymous said...

I don't even know how I ended up here, but I thought this post was good. I don't know who you are but definitely you're going to a famous blogger if you are not already ;) Cheers!

Also visit my web page; ERROR: The requested URL could not be retrieved

Anonymous said...

Hi! Would you mind if I share your blog with my
twitter group? There's a lot of folks that I think would really enjoy your content. Please let me know. Thank you

Here is my web blog: search engine optimization search engine marketing

Anonymous said...

whoah this weblog is fantastic i really like studying your posts.
Keep up the good work! You understand, many persons are looking around for this info,
you can help them greatly.

my weblog ... clickbank secrets
my page - Just click the up coming post

Anonymous said...

If some one wishes to be updated with most up-to-date technologies then he must be
pay a quick visit this site and be up to date everyday.

Have a look at my page; sofortkredite für arbeitslose
my site: schufafreies darlehen

Anonymous said...

I don't know whether it's just me or if everybody else experiencing problems with your
blog. It appears as if some of the text on your posts are running off the screen.
Can somebody else please provide feedback and let me know if this is
happening to them too? This may be a problem with my web browser
because I've had this happen previously. Thank you

Here is my web page search engine optimization company toronto

Anonymous said...

Wonderful article! We will be linking to this great article on our website.
Keep up the great writing.

my site: insurance business
Also see my web site: hiv life insurance

Anonymous said...

Right here is the right webpage for anybody
who hopes to find out about this topic. You know so much
its almost hard to argue with you (not that I personally will need
to…HaHa). You definitely put a new spin on a subject which has been discussed for years.
Great stuff, just great!

Feel free to surf to my weblog smew.dealsbookmarks.com

Anonymous said...

Yes! Finally someone writes about loan birmingham.


Also visit my page ... refinancing a home equity loan

Anonymous said...

Excellent web site you've got here.. It's difficult to
find quality writing like yours nowadays. I seriously appreciate people like you!
Take care!!

My homepage stay at home jobs in canada
Also see my webpage - Usuário:AugustaHO - clickn.com.br

Anonymous said...

Its such as you read my thoughts! You seem to know a lot about this, such as you wrote the e-book
in it or something. I feel that you simply
can do with some p.c. to power the message home a little bit, but instead of that, this is excellent
blog. A great read. I will definitely be back.

Look into my site; privare.org/Take-Action-To-Repair-Your-Credit-Score.htm

Anonymous said...

It's really a cool and helpful piece of information. I am satisfied that you just shared this helpful info with us. Please stay us informed like this. Thank you for sharing.

Look into my website - A Kiss from Escaflowne -

Anonymous said...

I blog frequently and I genuinely appreciate your information.
Your article has really peaked my interest. I'm going to bookmark your blog and keep checking for new information about once per week. I subscribed to your Feed too.

my blog post - chestfatburner.com

Anonymous said...

Nice post. I learn something totally new and challenging on blogs I stumbleupon everyday.
It's always interesting to read through articles from other writers and practice a little something from other websites.

my blog post The particular disadvantages regarding Growth hormone treatment - " moobs "

Anonymous said...

Howdy! This is my 1st comment here so I just wanted to give a quick shout out and say I truly
enjoy reading through your blog posts. Can you recommend any other blogs/websites/forums that deal with the same
subjects? Thanks for your time!

Also visit my weblog Utilizing testosterone carbamide peroxide teeth whitening serum with regard to gynecomastiatherapy?

Anonymous said...

Your style is so unique in comparison to other people I have read stuff from.
I appreciate you for posting when you've got the opportunity, Guess I'll just book mark this web site.


my blog post :: How You can get ready with regard to gynecomastiasurgical treatment

Anonymous said...

You are so interesting! I don't suppose I've read through anything like that before.
So great to discover someone with unique thoughts on this issue.
Really.. thank you for starting this up. This web site is something that's needed on the web, someone with a little originality!

Stop by my blog post ... Chestfatburner.Com

Anonymous said...

Somebody necessarily assist to make significantly posts I might state.
This is the very first time I frequented your web page
and up to now? I amazed with the research you made to create this
particular publish incredible. Magnificent job!

my web site ... clear

Anonymous said...

If you are going for finest contents like I do, only visit this site everyday as it presents
quality contents, thanks

my web page :: jay kubassek

Anonymous said...

Hello There. I found your blog using msn. This is
a really well written article. I will be sure to bookmark it and return to read more of your useful information.

Thanks for the post. I will certainly return.


Take a look at my blog :: http://www.sanpedrotelmo.com/points-to-consider-when-buying-plus-size-womens-clothing