/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * 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 version 2 of the License. * * 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.coregui.client.inventory.common.detail.operation.schedule; import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.user.client.rpc.AsyncCallback; import com.smartgwt.client.data.DSRequest; import com.smartgwt.client.data.Record; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.HTMLFlow; import com.smartgwt.client.widgets.form.events.ItemChangedEvent; import com.smartgwt.client.widgets.form.events.ItemChangedHandler; import com.smartgwt.client.widgets.form.fields.FormItem; import com.smartgwt.client.widgets.form.fields.SelectItem; import com.smartgwt.client.widgets.form.fields.StaticTextItem; import com.smartgwt.client.widgets.form.fields.TextAreaItem; import com.smartgwt.client.widgets.form.fields.events.ChangedEvent; import com.smartgwt.client.widgets.form.fields.events.ChangedHandler; import com.smartgwt.client.widgets.grid.ListGridRecord; import com.smartgwt.client.widgets.layout.VLayout; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.common.JobTrigger; import org.rhq.core.domain.common.ProductInfo; import org.rhq.core.domain.configuration.Configuration; import org.rhq.core.domain.configuration.ConfigurationUtility; import org.rhq.core.domain.configuration.definition.ConfigurationDefinition; import org.rhq.core.domain.configuration.definition.ConfigurationTemplate; import org.rhq.core.domain.operation.OperationDefinition; import org.rhq.core.domain.operation.OperationHistory; import org.rhq.core.domain.operation.bean.OperationSchedule; import org.rhq.core.domain.resource.ResourceType; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.UserSessionManager; import org.rhq.coregui.client.ViewPath; import org.rhq.coregui.client.components.configuration.ConfigurationEditor; import org.rhq.coregui.client.components.form.AbstractRecordEditor; import org.rhq.coregui.client.components.form.DurationItem; import org.rhq.coregui.client.components.form.EnhancedDynamicForm; import org.rhq.coregui.client.components.form.SortedSelectItem; import org.rhq.coregui.client.components.form.TimeUnit; import org.rhq.coregui.client.components.form.UnitType; import org.rhq.coregui.client.components.trigger.JobTriggerEditor; import org.rhq.coregui.client.gwt.ConfigurationGWTServiceAsync; import org.rhq.coregui.client.gwt.GWTServiceLookup; import org.rhq.coregui.client.inventory.common.detail.operation.schedule.AbstractOperationScheduleDataSource.Field; import org.rhq.coregui.client.inventory.groups.detail.ResourceGroupDetailView; import org.rhq.coregui.client.util.FormUtility; import org.rhq.coregui.client.util.TypeConversionUtility; import org.rhq.coregui.client.util.enhanced.EnhancedHLayout; import org.rhq.coregui.client.util.enhanced.EnhancedVLayout; import org.rhq.coregui.client.util.message.Message; import org.rhq.coregui.client.util.message.Message.Option; import org.rhq.coregui.client.util.message.Message.Severity; /** * A view for viewing or editing an RHQ {@link org.rhq.core.domain.operation.bean.OperationSchedule operation schedule}. * * @author Ian Springer */ public abstract class AbstractOperationScheduleDetailsView extends AbstractRecordEditor<AbstractOperationScheduleDataSource<? extends OperationSchedule>> { private static final String FIELD_OPERATION_DESCRIPTION = "operationDescription"; private static final String FIELD_OPERATION_PARAMETERS = "operationParameters"; private static final String PATH_EXAMPLE_PREFIX = "example="; protected static final int FIRST_COLUMN_WIDTH = 140; private Map<Integer, String> operationIdToNameMap = new HashMap<Integer, String>(); private Map<String, String> operationNameToDescriptionMap = new HashMap<String, String>(); private Map<String, ConfigurationDefinition> operationNameToParametersDefinitionMap = new HashMap<String, ConfigurationDefinition>(); private StaticTextItem operationDescriptionItem; private StaticTextItem operationParametersItem; private EnhancedHLayout operationParametersConfigurationHolder; private ConfigurationEditor operationParametersConfigurationEditor; private Configuration operationParameters; private JobTriggerEditor triggerEditor; private EnhancedDynamicForm notesForm; private Integer operationDefinitionId; private Integer operationExampleId; private ViewPath viewPath; private boolean isImmediateExecution; public AbstractOperationScheduleDetailsView( AbstractOperationScheduleDataSource<? extends OperationSchedule> dataSource, ResourceType resourceType, int scheduleId) { super(dataSource, scheduleId, MSG.view_operationScheduleDetails_operationSchedule(), null); this.setMembersMargin(5); Set<OperationDefinition> operationDefinitions = resourceType.getOperationDefinitions(); for (OperationDefinition operationDefinition : operationDefinitions) { this.operationIdToNameMap.put(operationDefinition.getId(), operationDefinition.getName()); this.operationNameToDescriptionMap.put(operationDefinition.getName(), operationDefinition.getDescription()); this.operationNameToParametersDefinitionMap.put(operationDefinition.getName(), operationDefinition.getParametersConfigurationDefinition()); } } protected abstract boolean hasControlPermission(); protected abstract int getResourceId(); /** * Returns the <code>id</code> of the {@link org.rhq.core.domain.operation.OperationHistory} which should serve * as an example entity when rescheduling an operation. The value comes from the view path when it has the following * form: <em>#Resource/10001/Operations/Schedules/0/example=10001</em>. * * <p>This method is not designed to be extended. It is used by subclasses during {@link #init(boolean)} to load * asynchronously the approriate (resource or group) example entity.</p> * * @return the {@link org.rhq.core.domain.operation.OperationHistory} example <code>id</code> when an operation is * being rescheduled, null otherwise * @see #getOperationExample() */ protected Integer getOperationExampleId() { return operationExampleId; } /** * Returns the {@link org.rhq.core.domain.operation.OperationHistory} which should serve as an example entity when * rescheduling an operation. Subclasses load the value asynchronously during {@link #init(boolean)}. * * @return the {@link org.rhq.core.domain.operation.OperationHistory} example when an operation is being * rescheduled, null otherwise * @see #getOperationExampleId() */ protected abstract OperationHistory getOperationExample(); @Override public String getListViewPath() { // If the operation is scheduled for immediate execution, we will send the user // to the history page so that he can view the status/result of the operation; // otherwise, the user will stay on the schedules list view. if (isImmediateExecution) { // If the operation is scheduled from the context menu, the view path will have // another entry appended to the end, the operation definition id (viewPath.getCurrentIndex() == 6) // Similarly, auto groups have another chunk of view path present ("AutoGroup"). // If the operation is scheduled for the auto group, the view path will include "Resource/AutoGroup" // This was causing BZ 823908 boolean isAutogroup = viewPath.getParentViewPath().contains(ResourceGroupDetailView.AUTO_GROUP_VIEW); if ((!isAutogroup && viewPath.getCurrentIndex() == 6) || (isAutogroup && viewPath.getCurrentIndex() == 7)) { return viewPath.getPathToIndex(viewPath.getCurrentIndex() - 3) + "/History"; } // common case (for resource, group not using context menu) return viewPath.getPathToIndex(viewPath.getCurrentIndex() - 2) + "/History"; } return super.getListViewPath(); } @Override public void renderView(ViewPath viewPath) { this.viewPath = viewPath; super.renderView(viewPath); operationDefinitionId = 0; if (!viewPath.isEnd()) { String currentPathPart = viewPath.getCurrent().getPath(); if (currentPathPart.startsWith(PATH_EXAMPLE_PREFIX)) { operationExampleId = Integer.valueOf(currentPathPart.substring(PATH_EXAMPLE_PREFIX.length())); viewPath.next(); } else { operationDefinitionId = viewPath.getCurrentAsInt(); viewPath.next(); } } // Existing schedules are not editable. This may change in the future. boolean isReadOnly = (!hasControlPermission() || (getRecordId() != 0)); // Note: subclases may override this and perform async work prior to calling super, so // be careful adding any code after this point in this method. init(isReadOnly); } @Override protected void init(boolean isReadOnly) { super.init(isReadOnly); OperationHistory operationExample = getOperationExample(); if (operationExample != null) { operationDefinitionId = operationExample.getOperationDefinition().getId(); initNameField(); getForm().rememberValues(); } else if (this.operationDefinitionId != null) { initNameField(); getForm().rememberValues(); } } private void initNameField() { FormItem nameField = getForm().getField(Field.OPERATION_NAME); nameField.setValue(operationIdToNameMap.get(operationDefinitionId)); handleOperationNameChange(OperationNameChangeContext.INIT); } @Override protected EnhancedDynamicForm buildForm() { EnhancedDynamicForm form = super.buildForm(); form.setNumCols(3); form.setColWidths(FIRST_COLUMN_WIDTH, "140", "*"); return form; } @Override protected List<FormItem> createFormItems(EnhancedDynamicForm form) { List<FormItem> items = new ArrayList<FormItem>(); if (!isNewRecord()) { StaticTextItem idItem = new StaticTextItem(Field.ID); items.add(idItem); } SelectItem operationNameItem = new SortedSelectItem(Field.OPERATION_NAME); operationNameItem.setShowTitle(true); items.add(operationNameItem); operationNameItem.addChangedHandler(new ChangedHandler() { @Override public void onChanged(ChangedEvent event) { handleOperationNameChange(OperationNameChangeContext.EDIT); } }); this.operationDescriptionItem = new StaticTextItem(FIELD_OPERATION_DESCRIPTION, MSG.common_title_description()); this.operationDescriptionItem.setShowTitle(false); items.add(this.operationDescriptionItem); this.operationParametersItem = new StaticTextItem(FIELD_OPERATION_PARAMETERS, MSG.view_operationScheduleDetails_field_parameters()); this.operationParametersItem.setColSpan(2); items.add(this.operationParametersItem); return items; } // override reset because we can't just blindly reset the op def name and leave behind associated widgets @Override protected void reset() { super.reset(); handleOperationNameChange(OperationNameChangeContext.RESET); } // The same logic needs to get applied to a user-initiated change, direct navigation to an op def, and // a reset, which may reset to a selected op def, or to no op def. private void handleOperationNameChange(OperationNameChangeContext context) { refreshOperationDescriptionItem(); refreshOperationParametersItem(context); if (null != getSelectedOperationName()) { onItemChanged(); } } @Override protected EnhancedVLayout buildContentPane() { EnhancedVLayout contentPane = super.buildContentPane(); this.operationParametersConfigurationHolder = new EnhancedHLayout(); this.operationParametersConfigurationHolder.setVisible(false); contentPane.addMember(this.operationParametersConfigurationHolder); HTMLFlow hr = new HTMLFlow("<hr/>"); contentPane.addMember(hr); this.triggerEditor = new JobTriggerEditor(isReadOnly()); contentPane.addMember(this.triggerEditor); hr = new HTMLFlow("<hr/>"); contentPane.addMember(hr); this.notesForm = new EnhancedDynamicForm(isReadOnly(), isNewRecord()); this.notesForm.setColWidths(FIRST_COLUMN_WIDTH, "50%", "140", "50%"); this.notesForm.addItemChangedHandler(new ItemChangedHandler() { @Override public void onItemChanged(ItemChangedEvent event) { AbstractOperationScheduleDetailsView.this.onItemChanged(); } }); List<FormItem> notesFields = new ArrayList<FormItem>(); TreeSet<TimeUnit> supportedUnits = new TreeSet<TimeUnit>(); supportedUnits.add(TimeUnit.SECONDS); supportedUnits.add(TimeUnit.MINUTES); supportedUnits.add(TimeUnit.HOURS); DurationItem timeoutItem = new DurationItem(Field.TIMEOUT, MSG.view_operationScheduleDetails_field_timeout(), supportedUnits, false, isReadOnly()); ProductInfo productInfo = CoreGUI.get().getProductInfo(); timeoutItem.setContextualHelp(MSG.view_operationScheduleDetails_fieldHelp_timeout(productInfo.getShortName())); notesFields.add(timeoutItem); if (!isNewRecord()) { StaticTextItem nextFireTimeItem = new StaticTextItem(Field.NEXT_FIRE_TIME, MSG.dataSource_operationSchedule_field_nextFireTime()); notesFields.add(nextFireTimeItem); } TextAreaItem notesItem = new TextAreaItem(Field.DESCRIPTION, MSG.dataSource_operationSchedule_field_description()); notesItem.setWidth(450); notesItem.setHeight(60); notesItem.setShowTitle(true); FormUtility.addContextualHelp(notesItem, MSG.view_operationScheduleDetails_fieldHelp_description()); notesFields.add(notesItem); this.notesForm.setFields(notesFields.toArray(new FormItem[notesFields.size()])); contentPane.addMember(this.notesForm); return contentPane; } @Override protected ButtonBar buildButtonBar() { ButtonBar buttonBar = super.buildButtonBar(); if (null != buttonBar) { buttonBar.getSaveButton().setTitle(MSG.common_button_schedule()); } return buttonBar; } @Override protected String getTitleFieldName() { return Field.OPERATION_DISPLAY_NAME; } @Override protected Record createNewRecord() { Record record = super.createNewRecord(); Subject sessionSubject = UserSessionManager.getSessionSubject(); record.setAttribute(Field.SUBJECT, sessionSubject.getName()); record.setAttribute(Field.SUBJECT_ID, sessionSubject.getId()); return record; } @Override protected void editRecord(Record record) { refreshOperationDescriptionItem(); refreshOperationParametersItem(OperationNameChangeContext.EDIT); super.editRecord(record); } @Override protected void editExistingRecord(Record record) { JavaScriptObject jobTriggerJavaScriptObject = (JavaScriptObject) getForm().getValue(Field.JOB_TRIGGER); Record jobTriggerRecord = new ListGridRecord(jobTriggerJavaScriptObject); JobTrigger jobTrigger = getDataSource().createJobTrigger(jobTriggerRecord); this.triggerEditor.setJobTrigger(jobTrigger); FormItem nextFireTimeItem = this.notesForm.getField(Field.NEXT_FIRE_TIME); nextFireTimeItem.setValue(getForm().getValue(Field.NEXT_FIRE_TIME)); DurationItem timeoutItem = (DurationItem) this.notesForm.getField(Field.TIMEOUT); Object value = getForm().getValue(Field.TIMEOUT); Integer integerValue = TypeConversionUtility.toInteger(value); timeoutItem.setValue(integerValue, UnitType.TIME); StaticTextItem notesItem = (StaticTextItem) this.notesForm.getField(Field.DESCRIPTION); // Notes field is user-editable, so escape HTML to prevent an XSS attack. Unless empty, then don't to prevent // displaying   as the value. String notesValue = getForm().getValueAsString(Field.DESCRIPTION); if (null != notesValue && !notesValue.isEmpty()) { notesItem.setEscapeHTML(true); } notesItem.setValue(notesValue); this.operationParameters = (Configuration) record.getAttributeAsObject(Field.PARAMETERS); super.editExistingRecord(record); } @Override protected void save(DSRequest requestProperties) { if ((null != this.operationParametersConfigurationEditor && !this.operationParametersConfigurationEditor .isValid()) || !this.triggerEditor.validate()) { { Message message = new Message(MSG.widget_recordEditor_warn_validation(this.getDataTypeName()), Severity.Warning, EnumSet.of(Option.Transient)); CoreGUI.getMessageCenter().notify(message); return; } } requestProperties.setAttribute(AbstractOperationScheduleDataSource.RequestProperty.PARAMETERS, this.operationParameters); EnhancedDynamicForm form = getForm(); Record jobTriggerRecord = new ListGridRecord(); Date startTime = this.triggerEditor.getStartTime(); isImmediateExecution = startTime == null; jobTriggerRecord.setAttribute(Field.START_TIME, startTime); Date endTime = this.triggerEditor.getEndTime(); jobTriggerRecord.setAttribute(Field.END_TIME, endTime); Integer repeatCount = this.triggerEditor.getRepeatCount(); jobTriggerRecord.setAttribute(Field.REPEAT_COUNT, repeatCount); Long repeatInterval = this.triggerEditor.getRepeatInterval(); jobTriggerRecord.setAttribute(Field.REPEAT_INTERVAL, repeatInterval); String cronExpression = this.triggerEditor.getCronExpression(); jobTriggerRecord.setAttribute(Field.CRON_EXPRESSION, cronExpression); form.setValue(Field.JOB_TRIGGER, jobTriggerRecord); DurationItem timeoutItem = (DurationItem) this.notesForm.getItem(Field.TIMEOUT); Long timeout = timeoutItem.getValueAsLong(); if (timeout != null) { form.setValue(Field.TIMEOUT, timeout); } else { form.setValue(Field.TIMEOUT, (String) null); } FormItem notesItem = this.notesForm.getField(Field.DESCRIPTION); form.setValue(Field.DESCRIPTION, (String) notesItem.getValue()); // force NO-REFESH once form is saved requestProperties.setAttribute(FIELD_NO_REFRESH, true); super.save(requestProperties); } private void refreshOperationDescriptionItem() { String operationName = getSelectedOperationName(); String value; if (operationName == null) { value = "<i>" + MSG.view_operationScheduleDetails_fieldDefault_description() + "</i>"; } else { value = this.operationNameToDescriptionMap.get(operationName); } this.operationDescriptionItem.setValue(value); } private void refreshOperationParametersItem(OperationNameChangeContext operationNameChangeContext) { String operationName = getSelectedOperationName(); String value; if (isNewRecord()) { // BZ 909157: do it only for new schedule operationParameters = null; // reset params between dropdown selects // make sure we wipe out anything left by the previous op def for (Canvas child : this.operationParametersConfigurationHolder.getChildren()) { child.destroy(); } } if (operationName == null) { value = "<i>" + MSG.view_operationScheduleDetails_fieldDefault_parameters() + "</i>"; this.operationParametersConfigurationHolder.hide(); } else { final ConfigurationDefinition parametersDefinition = this.operationNameToParametersDefinitionMap .get(operationName); if (parametersDefinition == null || parametersDefinition.getPropertyDefinitions().isEmpty()) { value = "<i>" + MSG.view_operationScheduleDetails_noParameters() + "</i>"; this.operationParametersConfigurationHolder.hide(); } else { value = isNewRecord() ? "<i>" + MSG.view_operationScheduleDetails_enterParametersBelow() + "</i>" : ""; // Add spacer so params are indented. VLayout horizontalSpacer = new VLayout(); horizontalSpacer.setWidth(FIRST_COLUMN_WIDTH); this.operationParametersConfigurationHolder.addMember(horizontalSpacer); if (isNewRecord()) { ConfigurationTemplate defaultTemplate = parametersDefinition.getDefaultTemplate(); switch (operationNameChangeContext) { case INIT: case RESET: OperationHistory operationExample = getOperationExample(); if (operationExample != null) { Configuration exampleParameters = operationExample.getParameters(); if (exampleParameters == null) { Message message = new Message( MSG.view_operationScheduleDetails_example_null_parameters(), Severity.Warning); CoreGUI.getMessageCenter().notify(message); } else if (!ConfigurationUtility.validateConfiguration(exampleParameters, parametersDefinition).isEmpty()) { Message message = new Message( MSG.view_operationScheduleDetails_example_parameters_invalid(), Severity.Warning); CoreGUI.getMessageCenter().notify(message); } else { operationParameters = exampleParameters.deepCopy(false); break; } } default: operationParameters = (defaultTemplate != null) ? defaultTemplate.createConfiguration() : new Configuration(); } } ConfigurationGWTServiceAsync configurationService = GWTServiceLookup.getConfigurationService(); configurationService.getOptionValuesForConfigDefinition(getResourceId(), -1, parametersDefinition, new AsyncCallback<ConfigurationDefinition>() { @Override public void onFailure(Throwable throwable) { operationParametersConfigurationEditor = new ConfigurationEditor(parametersDefinition, operationParameters); operationParametersConfigurationEditor.setReadOnly(isReadOnly()); operationParametersConfigurationHolder.addMember(operationParametersConfigurationEditor); operationParametersConfigurationHolder.show(); } @Override public void onSuccess(ConfigurationDefinition result) { operationParametersConfigurationEditor = new ConfigurationEditor(result, operationParameters); operationParametersConfigurationEditor.setReadOnly(isReadOnly()); operationParametersConfigurationHolder.addMember(operationParametersConfigurationEditor); operationParametersConfigurationHolder.show(); } }); } } this.operationParametersItem.setValue(value); } private String getSelectedOperationName() { FormItem operationNameItem = getForm().getField(Field.OPERATION_NAME); return (String) operationNameItem.getValue(); } private enum OperationNameChangeContext { INIT, EDIT, RESET } }