/*
*
* This is a simple Email Queue management system
* Copyright (C) 2011 Imran M Yousuf (imyousuf@smartitengineering.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.smartitengineering.generator.engine.service.impl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.smartitengineering.cms.api.content.FieldValue;
import com.smartitengineering.cms.api.content.MutableCollectionFieldValue;
import com.smartitengineering.cms.api.content.MutableCompositeFieldValue;
import com.smartitengineering.cms.api.content.MutableField;
import com.smartitengineering.cms.api.content.MutableStringFieldValue;
import com.smartitengineering.cms.api.content.Representation;
import com.smartitengineering.cms.api.factory.SmartContentAPI;
import com.smartitengineering.cms.api.factory.content.ContentLoader;
import com.smartitengineering.cms.api.factory.content.WriteableContent;
import com.smartitengineering.cms.api.type.CollectionDataType;
import com.smartitengineering.cms.api.type.CompositeDataType;
import com.smartitengineering.cms.api.type.ContentType;
import com.smartitengineering.cms.api.type.ContentTypeId;
import com.smartitengineering.cms.api.type.FieldDef;
import com.smartitengineering.cms.api.workspace.WorkspaceId;
import com.smartitengineering.dao.common.CommonReadDao;
import com.smartitengineering.dao.common.CommonWriteDao;
import com.smartitengineering.dao.common.queryparam.MatchMode;
import com.smartitengineering.dao.common.queryparam.QueryParameter;
import com.smartitengineering.dao.common.queryparam.QueryParameterFactory;
import com.smartitengineering.emailq.domain.Email;
import com.smartitengineering.emailq.service.Services;
import com.smartitengineering.generator.engine.domain.CodeOnDemand;
import com.smartitengineering.generator.engine.domain.Map;
import com.smartitengineering.generator.engine.domain.Map.Entries;
import com.smartitengineering.generator.engine.domain.Report;
import com.smartitengineering.generator.engine.domain.ReportConfig;
import com.smartitengineering.generator.engine.domain.ReportConfig.EmailConfig;
import com.smartitengineering.generator.engine.domain.ReportEvent;
import com.smartitengineering.generator.engine.domain.SourceCode;
import com.smartitengineering.generator.engine.domain.SourceCode.Code;
import com.smartitengineering.generator.engine.service.ReportConfigFilter;
import com.smartitengineering.generator.engine.service.ReportConfigService;
import com.smartitengineering.generator.engine.service.ReportExecutor;
import com.smartitengineering.generator.engine.service.factory.ContentUtils;
import com.smartitengineering.generator.engine.service.impl.scripting.GroovyObjectFactory;
import com.smartitengineering.generator.engine.service.impl.scripting.JRubyObjectFactory;
import com.smartitengineering.generator.engine.service.impl.scripting.JavascriptObjectFactory;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
import org.apache.commons.lang.StringUtils;
import org.quartz.CronExpression;
import org.quartz.DateIntervalTrigger;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
* @author saumitra
*/
@Singleton
public class ReportConfigServiceImpl implements ReportConfigService {
private static final int DEFAULT_SCHEDULE_SIZE = 100;
public static final String INJECT_NAME_NUM_OF_SCHEDULES = "numberOfSchedulesToReside";
public static final String INJECT_NAME_EXECUTION_SERVICE = "reportGenerationExec";
public static final String FROM_ADDRESS = "fromAddressString";
public static final String DEFAULT_BODY = "defaultBodyString";
public static final String MIME_TYPE_FILE_EXT_MAP = "mimeTypeFileExtMap";
@Inject
protected CommonReadDao<ReportConfig, String> commonReadDao;
@Inject
protected CommonWriteDao<ReportConfig> commonWriteDao;
@Inject
protected CommonReadDao<ReportEvent, String> commonEventReadDao;
@Inject
protected CommonWriteDao<ReportEvent> commonEventWriteDao;
@Inject(optional = true)
@Named(INJECT_NAME_NUM_OF_SCHEDULES)
protected int defaultScheduleSize;
@Inject
@Named(ReportServiceImpl.INJECT_NAME_WORKSPACE_ID)
private WorkspaceId workspaceId;
@Inject
@Named(ReportServiceImpl.INJECT_NAME_REPORT_CONTENT_TYPE_ID)
private ContentTypeId reportTypeId;
@Inject
@Named(INJECT_NAME_EXECUTION_SERVICE)
private ExecutorService executor;
@Inject
@Named(FROM_ADDRESS)
private String fromAddress;
@Inject
@Named(DEFAULT_BODY)
private String defaultBody;
@Inject
@Named(MIME_TYPE_FILE_EXT_MAP)
private java.util.Map<String, String> mimeTypeFileExtMap;
protected final transient Logger logger = LoggerFactory.getLogger(getClass());
private final Semaphore syncMutex = new Semaphore(1);
private final Semaphore reportMutex = new Semaphore(1);
private final Semaphore resyncMutex = new Semaphore(1);
private Scheduler scheduler;
@Override
public ReportConfig getById(String id) {
if (StringUtils.isBlank(id)) {
return null;
}
return commonReadDao.getById(id);
}
@Override
public void save(ReportConfig reportConfig) {
if (reportConfig.getEmbeddedSourceCode() == null && reportConfig.getCodeOnDemand() == null) {
throw new IllegalArgumentException("No code specified!");
}
if (reportConfig.getEmbeddedSourceCode() != null && (reportConfig.getEmbeddedSourceCode().getCode() == null ||
StringUtils.isBlank(reportConfig.getEmbeddedSourceCode().
getCode().getEmbeddedCode()) || reportConfig.
getEmbeddedSourceCode().getCode().getCodeType() == null)) {
throw new IllegalArgumentException("Embedded source code not specified properly!");
}
if (getSchedules(reportConfig, null) == null) {
throw new IllegalArgumentException("No schedules from report!");
}
resetScheduleGeneration(reportConfig);
commonWriteDao.save(reportConfig);
}
@Override
public void delete(ReportConfig reportConfig) {
commonWriteDao.delete(reportConfig);
deleteConfigEvents(reportConfig);
}
@Override
public void update(ReportConfig reportConfig) {
commonWriteDao.update(reportConfig);
}
@Override
public void scheduleReport(ReportConfig config, Date schedule) {
if (config == null) {
return;
}
if (schedule == null) {
schedule = new Date();
}
createReportEvent(schedule, config);
}
@Inject
public void initCrons() {
logger.info("Initialize cron jobs");
try {
scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail detail = new JobDetail("reportSyncJob", "reportSyncPoll", EventSyncJob.class);
Trigger trigger = new DateIntervalTrigger("reportSyncTrigger", "reportSyncPoll",
DateIntervalTrigger.IntervalUnit.MINUTE,
5);
JobDetail redetail = new JobDetail("reportReSyncJob", "reportReSyncPoll", EventReSyncJob.class);
Trigger retrigger = new DateIntervalTrigger("reportReSyncTrigger", "reportReSyncPoll",
DateIntervalTrigger.IntervalUnit.DAY, 1);
JobDetail reportDetail = new JobDetail("reportJob", "reportPoll", ReportJob.class);
Trigger reportTrigger = new DateIntervalTrigger("reportTrigger", "reportPoll",
DateIntervalTrigger.IntervalUnit.MINUTE, 1);
scheduler.setJobFactory(new JobFactory() {
public Job newJob(TriggerFiredBundle bundle) throws SchedulerException {
try {
Class<? extends Job> jobClass = bundle.getJobDetail().getJobClass();
if (ReportConfigServiceImpl.class.equals(jobClass.getEnclosingClass())) {
Constructor<? extends Job> constructor =
(Constructor<? extends Job>) jobClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Job job = constructor.newInstance(ReportConfigServiceImpl.this);
return job;
}
else {
return jobClass.newInstance();
}
}
catch (Exception ex) {
throw new SchedulerException(ex);
}
}
});
scheduler.start();
scheduler.scheduleJob(detail, trigger);
scheduler.scheduleJob(redetail, retrigger);
scheduler.scheduleJob(reportDetail, reportTrigger);
}
catch (Exception ex) {
logger.warn("Could initialize cron job!", ex);
}
}
private class ReportJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
reportMutex.acquire();
}
catch (Exception ex) {
logger.warn("Could not acquire lock!", ex);
throw new JobExecutionException(ex);
}
generateReport();
reportMutex.release();
}
}
private class EventSyncJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
syncMutex.acquire();
}
catch (Exception ex) {
logger.warn("Could not acquire lock!", ex);
throw new JobExecutionException(ex);
}
publishReportEventsForConfig();
syncMutex.release();
}
}
private class EventReSyncJob implements Job {
public void execute(JobExecutionContext context) throws JobExecutionException {
try {
resyncMutex.acquire();
}
catch (Exception ex) {
logger.warn("Could not acquire lock!", ex);
throw new JobExecutionException(ex);
}
reSyncReportEventsForConfig();
resyncMutex.release();
}
}
private class GenerateReport implements Runnable {
private final ReportEvent reportEvent;
public GenerateReport(ReportEvent reportEvent) {
if (reportEvent == null) {
throw new IllegalArgumentException("ReportEvent can not be null!");
}
if (reportEvent.getReportConfig() == null) {
throw new IllegalStateException("Config for report event is missing!");
}
this.reportEvent = reportEvent;
}
public void run() {
reportEvent.setEventStatus(ReportEvent.EventStatus.In_Progress);
commonEventWriteDao.update(reportEvent);
try {
final ReportConfig reportConfig = reportEvent.getReportConfig();
ReportExecutor executor = getExecutor(reportConfig);
final Map params = reportConfig.getParams();
if (params != null && params.getEntries() != null) {
final java.util.Map<String, String> paramMap = new LinkedHashMap<String, String>(params.getEntries().size());
for (Map.Entries entries : params.getEntries()) {
paramMap.put(entries.getKey(), entries.getValue());
}
long startDate = System.currentTimeMillis();
WriteableContent content = executor.createReport(workspaceId, reportEvent.getDateReportScheduledFor(),
paramMap);
long endDate = System.currentTimeMillis();
if (isInstanceOf(content.getContentDefinition(), reportTypeId)) {
ContentLoader loader = SmartContentAPI.getInstance().getContentLoader();
final java.util.Map<String, FieldDef> fieldDefs = reportTypeId.getContentType().getFieldDefs();
//Exec start datetime
content.setField(
ContentUtils.getField(Report.PROPERTY_EXECUTIONSTARTDATE, reportTypeId, new Date(startDate)));
//Exec end datetime
content.setField(ContentUtils.getField(Report.PROPERTY_EXECUTIONENDDATE, reportTypeId, new Date(endDate)));
//Trigger datetime
ContentUtils.getField(Report.PROPERTY_TRIGGERDATE, reportTypeId, reportEvent.getDateReportScheduledFor());
//Params
Map map = reportConfig.getParams();
if (map != null && content.getField(Report.PROPERTY_PARAMS) == null && map.getEntries() != null && !map.
getEntries().isEmpty()) {
FieldDef def = fieldDefs.get(Report.PROPERTY_PARAMS);
CompositeDataType compositeDataType = ((CompositeDataType) def.getValueDef());
FieldDef mapDef = compositeDataType.getComposedFieldDefs().get(Map.PROPERTY_ENTRIES);
CompositeDataType entryType = ((CompositeDataType) ((CollectionDataType) mapDef.getValueDef()).
getItemDataType());
final Collection<Entries> entriess = map.getEntries();
Collection<FieldValue> vals = new ArrayList<FieldValue>(entriess.size());
for (Entries entries : entriess) {
MutableStringFieldValue keyVal = loader.createStringFieldValue();
keyVal.setValue(entries.getKey());
MutableField keyField = loader.createMutableField(null, entryType.getComposedFieldDefs().get(
Map.Entries.PROPERTY_KEY));
keyField.setValue(keyVal);
MutableStringFieldValue valVal = loader.createStringFieldValue();
valVal.setValue(entries.getValue());
MutableField valField = loader.createMutableField(null, entryType.getComposedFieldDefs().get(
Map.Entries.PROPERTY_VALUE));
valField.setValue(valVal);
MutableCompositeFieldValue entryVal = loader.createCompositeFieldValue();
entryVal.setField(keyField);
entryVal.setField(valField);
vals.add(entryVal);
}
MutableCollectionFieldValue cVal = loader.createCollectionFieldValue();
cVal.setValue(vals);
MutableField compField = loader.createMutableField(null, mapDef);
compField.setValue(cVal);
content.setField(ContentUtils.getField(Report.PROPERTY_PARAMS, reportTypeId, Collections.singleton(
compField)));
}
//Config
content.setField(ContentUtils.getField(Report.PROPERTY_REPORTEVENT, reportTypeId, ReportServiceImpl.
getContentId(workspaceId, reportEvent.getId())));
//Save content
content.put();
//Email representation as per config
Collection<EmailConfig> emailConfigs = reportConfig.getEmailConfig();
for (EmailConfig emailConfig : emailConfigs) {
final String representationName = emailConfig.getRepresentationName();
if (StringUtils.isNotBlank(representationName)) {
Representation representation = content.getRepresentation(representationName);
final byte[] representationData = representation != null ? representation.getRepresentation() : null;
if (representationData != null && representationData.length > 0) {
Email email = new Email();
final String mimeType = representation.getMimeType();
if ("text/plain".equals(mimeType)) {
Email.Message message = new Email.Message();
message.setMsgType(Email.Message.MsgType.PLAIN);
message.setMsgBody(new String(representationData));
email.setMessage(message);
}
else if ("text/html".equals(mimeType)) {
Email.Message message = new Email.Message();
message.setMsgType(Email.Message.MsgType.HTML);
message.setMsgBody(new String(representationData));
email.setMessage(message);
}
else {
Email.Attachments attachment = new Email.Attachments();
attachment.setContentType(mimeType);
String reportTitle = content.getField(Report.PROPERTY_NAME).getValue().toString().replaceAll(
"\\s+", "_");
StringBuilder attachmentName = new StringBuilder(reportTitle);
if (mimeTypeFileExtMap.containsKey(mimeType)) {
attachmentName.append('.').append(mimeTypeFileExtMap.get(mimeType));
}
attachment.setName(attachmentName.toString());
attachment.setDescription("Report");
attachment.setBlob(representationData);
email.setAttachments(Arrays.asList(attachment));
Email.Message message = new Email.Message();
message.setMsgType(Email.Message.MsgType.PLAIN);
message.setMsgBody(defaultBody);
email.setMessage(message);
}
email.setSubject(emailConfig.getSubject());
email.setFrom(fromAddress);
email.setTo(emailConfig.getTo());
email.setCc(emailConfig.getCc());
email.setBcc(emailConfig.getBcc());
try {
Services.getInstance().getEmailService().saveEmail(email);
}
catch (Exception ex) {
logger.error("Could put email to the queue", ex);
}
}
}
}
}
else {
throw new IllegalStateException("Content created as REPORT ain't instance of " + reportTypeId);
}
}
reportEvent.setEventStatus(ReportEvent.EventStatus.Finished);
commonEventWriteDao.update(reportEvent);
}
catch (Exception ex) {
logger.error("Error while executing report!", ex);
reportEvent.setEventStatus(ReportEvent.EventStatus.Error);
StringWriter writer = new StringWriter();
ex.printStackTrace(new PrintWriter(writer));
reportEvent.setAdditionalStatusInfo(writer.toString());
commonEventWriteDao.update(reportEvent);
}
}
}
protected ReportEvent createReportEvent(Date schedule, ReportConfig config) {
ReportEvent event = new ReportEvent();
event.setDateReportScheduledFor(schedule);
event.setEventStatus(ReportEvent.EventStatus.Pending);
event.setReportConfig(config);
commonEventWriteDao.save(event);
return event;
}
protected ReportExecutor getExecutor(ReportConfig config) {
CodeOnDemand demand = config.getCodeOnDemand();
if (demand != null) {
SourceCode code = demand.getCode();
return processSourceCodeToExecutor(code);
}
else {
SourceCode embeddedCode = config.getEmbeddedSourceCode();
return processSourceCodeToExecutor(embeddedCode);
}
}
protected ReportExecutor processSourceCodeToExecutor(SourceCode sourceCode) throws IllegalArgumentException,
IllegalStateException {
if (sourceCode != null) {
try {
Code code = sourceCode.getCode();
final ReportExecutor reportExecutor;
byte[] codeData = org.apache.commons.codec.binary.StringUtils.getBytesUtf8(code.getEmbeddedCode());
switch (code.getCodeType()) {
case GROOVY:
reportExecutor = GroovyObjectFactory.getInstance().getObjectFromScript(codeData, ReportExecutor.class);
break;
case RUBY:
reportExecutor = JRubyObjectFactory.getInstance().getObjectFromScript(codeData, ReportExecutor.class);
break;
case JAVASCRIPT:
reportExecutor = JavascriptObjectFactory.getInstance().getObjectFromScript(codeData, ReportExecutor.class);
break;
default:
reportExecutor = null;
}
return reportExecutor;
}
catch (Exception ex) {
throw new IllegalStateException("Could not convert script!", ex);
}
}
else {
throw new IllegalArgumentException("Source code can not be null!");
}
}
protected boolean isInstanceOf(ContentType type, final ContentTypeId typeDef) {
boolean isInstanceOf = false;
if (type.getContentTypeID().equals(typeDef)) {
isInstanceOf = true;
}
if (!isInstanceOf) {
ContentTypeId parentId = type.getParent();
final ContentType parentType = parentId.getContentType();
if (!isInstanceOf && parentId != null && parentType != null) {
isInstanceOf = isInstanceOf(parentType, typeDef);
}
}
return isInstanceOf;
}
protected void generateReport() {
try {
List<ReportEvent> events = commonEventReadDao.getList(QueryParameterFactory.getEqualPropertyParam(
ReportEvent.PROPERTY_EVENTSTATUS, ReportEvent.EventStatus.Pending.name()), QueryParameterFactory.
getLesserThanEqualToPropertyParam(ReportEvent.PROPERTY_DATEREPORTSCHEDULEDFOR, new Date()));
if (events != null && !events.isEmpty()) {
for (ReportEvent event : events) {
executor.submit(new GenerateReport(event));
}
}
}
catch (Exception ex) {
logger.error("Cpuld not generate report!", ex);
}
}
protected void publishReportEventsForConfig() {
if (logger.isInfoEnabled()) {
logger.info("Tracking all reports to be for which a schedule is required");
}
List<ReportConfig> configs;
try {
configs = commonReadDao.getList(QueryParameterFactory.getEqualPropertyParam(
ReportConfig.PROPERTY_EVENTSCREATED, false));
}
catch (Exception ex) {
logger.warn("Could not get to be processed reports configs!", ex);
configs = null;
}
createFreshSchedules(configs, true);
}
protected void reSyncReportEventsForConfig() {
if (logger.isInfoEnabled()) {
logger.info("Tracking all reports to be for which a schedule is required");
}
List<ReportConfig> configs;
Calendar date = Calendar.getInstance();
date.setTime(new Date());
date.add(Calendar.DATE, 3);
try {
configs = commonReadDao.getList(QueryParameterFactory.getEqualPropertyParam(ReportConfig.PROPERTY_EVENTSCREATED,
true), QueryParameterFactory.
getLesserThanEqualToPropertyParam(ReportConfig.PROPERTY_VALIDTILL, date.getTime()));
}
catch (Exception ex) {
logger.warn("Could not get to be processed reports configs!", ex);
configs = null;
}
createFreshSchedules(configs, false);
}
protected void createFreshSchedules(List<ReportConfig> configs, boolean deleteOlds) {
if (configs != null && !configs.isEmpty()) {
for (ReportConfig config : configs) {
//Delete the old events
if (deleteOlds) {
if (!deleteConfigEvents(config)) {
continue;
}
}
try {
final List<Date> schedules = getSchedules(config, config.getValidTill());
if (schedules != null && !schedules.isEmpty()) {
//Create the new events
for (Date schedule : schedules) {
createReportEvent(schedule, config);
}
config.setEventsCreated(true);
config.setValidTill(schedules.get(schedules.size() - 1));
commonWriteDao.update(config);
}
}
catch (Exception ex) {
logger.error("Could not create all events for config " + ReportServiceImpl.getContentId(workspaceId, config.
getId()), ex);
}
}
}
}
protected boolean deleteConfigEvents(ReportConfig config) {
try {
List<ReportEvent> events = commonEventReadDao.getList(QueryParameterFactory.getEqualPropertyParam(
ReportEvent.PROPERTY_EVENTSTATUS, ReportEvent.EventStatus.Pending.name()), QueryParameterFactory.
getStringLikePropertyParam(ReportEvent.PROPERTY_REPORTCONFIG, ReportServiceImpl.getContentId(workspaceId,
config.getId()).
toString()));
if (events != null && !events.isEmpty()) {
commonEventWriteDao.delete(events.toArray(new ReportEvent[events.size()]));
}
return true;
}
catch (Exception ex) {
logger.warn("Could not delete old events!", ex);
logger.error("As error in deleting old events skipping the config for this iteration!");
return false;
}
}
protected int getScheduleSize() {
return defaultScheduleSize > 0 ? defaultScheduleSize : DEFAULT_SCHEDULE_SIZE;
}
protected List<Date> getSchedules(ReportConfig config, final Date startDate) {
try {
CronExpression expression = new CronExpression(config.getTrigger());
List<Date> schedules = new ArrayList<Date>(getScheduleSize());
Date baseDate = startDate == null ? new Date() : startDate;
for (int i = 0; i < getScheduleSize(); ++i) {
final Date nextDate = expression.getNextValidTimeAfter(baseDate);
schedules.add(nextDate);
baseDate = nextDate;
}
return schedules;
}
catch (ParseException ex) {
logger.warn("Could not parse expression!", ex);
throw new IllegalStateException(ex);
}
catch (Exception ex) {
logger.warn("Could not create schedules!", ex);
throw new IllegalStateException(ex);
}
}
@Override
public Collection<ReportConfig> searchConfigs(ReportConfigFilter filter) {
List<QueryParameter> queries = new ArrayList<QueryParameter>();
if (filter.getCount() > 0) {
queries.add(QueryParameterFactory.getMaxResultsParam(filter.getCount()));
}
if (filter.getPageIndex() > -1 && filter.getCount() > 0) {
queries.add(QueryParameterFactory.getFirstResultParam(filter.getCount() * filter.getPageIndex()));
}
if (filter.getCreated() != null) {
queries.add(QueryParameterFactory.getGreaterThanEqualToPropertyParam("creationDate", filter.getCreated()));
}
if (StringUtils.isNotBlank(filter.getNameLike())) {
queries.add(QueryParameterFactory.getStringLikePropertyParam(ReportConfig.PROPERTY_NAME, filter.getNameLike(),
MatchMode.ANYWHERE));
}
return commonReadDao.getList(queries);
}
private void resetScheduleGeneration(ReportConfig reportConfig) {
reportConfig.setEventsCreated(Boolean.FALSE);
reportConfig.setValidTill(new Date());
final List<Date> schedules = getSchedules(reportConfig, null);
if (schedules == null || schedules.isEmpty()) {
throw new IllegalArgumentException("Config does not have any schedules!");
}
}
}