package sk.stuba.fiit.perconik.core.ui.preferences; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import com.google.common.base.Optional; import com.google.common.base.StandardSystemProperty; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.Service.State; import org.eclipse.core.runtime.IProduct; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.DialogSettings; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.StatusDialog; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.jface.layout.TableColumnLayout; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.ITableLabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import sk.stuba.fiit.perconik.core.Nameable; import sk.stuba.fiit.perconik.core.services.Service; import sk.stuba.fiit.perconik.core.services.ServiceListener; import sk.stuba.fiit.perconik.core.services.Services; import sk.stuba.fiit.perconik.core.services.listeners.ListenerService; import sk.stuba.fiit.perconik.core.services.resources.ResourceService; import sk.stuba.fiit.perconik.core.ui.plugin.Activator; import sk.stuba.fiit.perconik.eclipse.core.runtime.Products; import sk.stuba.fiit.perconik.eclipse.jface.viewers.ElementComparers; import sk.stuba.fiit.perconik.eclipse.jface.viewers.MapContentProvider; import sk.stuba.fiit.perconik.eclipse.jface.viewers.RegularTableViewer; import sk.stuba.fiit.perconik.eclipse.swt.widgets.WidgetListener; import sk.stuba.fiit.perconik.environment.Environment; import sk.stuba.fiit.perconik.ui.Buttons; import sk.stuba.fiit.perconik.ui.Groups; import sk.stuba.fiit.perconik.ui.Labels; import sk.stuba.fiit.perconik.ui.TableColumns; import sk.stuba.fiit.perconik.ui.Tables; import sk.stuba.fiit.perconik.ui.preferences.AbstractWorkbenchPreferencePage; import sk.stuba.fiit.perconik.utilities.SmartStringBuilder; import sk.stuba.fiit.perconik.utilities.concurrent.TimeValue; import static java.lang.Integer.toHexString; import static java.lang.String.format; import static java.lang.System.identityHashCode; import static java.util.Collections.emptyMap; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static com.google.common.base.Optional.absent; import static com.google.common.base.Optional.of; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.collect.Sets.immutableEnumSet; import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; import static org.eclipse.jface.dialogs.MessageDialog.openError; import static sk.stuba.fiit.perconik.core.plugin.Activator.awaitServices; import static sk.stuba.fiit.perconik.core.plugin.Activator.defaultInstance; import static sk.stuba.fiit.perconik.core.plugin.Activator.loadServices; import static sk.stuba.fiit.perconik.core.plugin.Activator.loadedServices; import static sk.stuba.fiit.perconik.core.plugin.Activator.unloadServices; import static sk.stuba.fiit.perconik.utilities.MoreStrings.firstNonNullOrEmpty; import static sk.stuba.fiit.perconik.utilities.MoreStrings.toLowerCase; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.optionEquivalence; import static sk.stuba.fiit.perconik.utilities.configuration.Configurables.rawOptionType; public final class ServicesPreferencePage extends AbstractWorkbenchPreferencePage { static final TimeValue awaitServicesTimeout = TimeValue.of(12, SECONDS); static final TimeValue loadServicesTimeout = TimeValue.of(8, SECONDS); static final TimeValue unloadServicesTimeout = TimeValue.of(16, SECONDS); static final TimeValue stateTransitionDisplayPause = TimeValue.of(200, MILLISECONDS); static final ImmutableSet<State> terminalStates = immutableEnumSet(State.TERMINATED, State.FAILED); Button load; Button unload; DetailsDialog detailsDialog; Label resourceLabel; Label listenerLabel; Button resourceButton; Button listenerButton; public ServicesPreferencePage() {} @Override public final void createControl(final Composite parent) { super.createControl(parent); this.updatePage(); } @Override protected Control createContent(final Composite parent) { this.initializeDialogUnits(parent); this.noDefaultAndApplyButton(); Composite composite = new Composite(parent, SWT.NONE); composite.setLayout(GridLayoutFactory.fillDefaults().create()); Group environmentGroup = Groups.create(composite, "Environment"); environmentGroup.setLayout(new GridLayout(2, false)); Labels.create(environmentGroup, environmentText()); Optional<ResourceService> resourceService = resourceService(); Optional<ListenerService> listenerService = listenerService(); this.detailsDialog = new DetailsDialog(this.getShell()); Group resourceGroup = Groups.create(composite, "Resource Service"); Group listenerGroup = Groups.create(composite, "Listener Service"); resourceGroup.setLayout(new GridLayout(2, false)); listenerGroup.setLayout(new GridLayout(2, false)); this.resourceLabel = Labels.create(resourceGroup, toState(resourceService)); this.listenerLabel = Labels.create(listenerGroup, toState(listenerService)); this.resourceButton = Buttons.create(resourceGroup, "Details", new WidgetListener() { public void handleEvent(final Event event) { DetailsDialog dialog = ServicesPreferencePage.this.detailsDialog; dialog.setTitle("Resource Service Details"); dialog.setComponents(toResourceComponents(resourceService())); dialog.open(); } }); this.listenerButton = Buttons.create(listenerGroup, "Details", new WidgetListener() { public void handleEvent(final Event event) { DetailsDialog dialog = ServicesPreferencePage.this.detailsDialog; dialog.setTitle("Listener Service Details"); dialog.setComponents(toListenerComponents(listenerService())); dialog.open(); } }); Dialog.applyDialogFont(composite); return composite; } @Override protected void contributeButtons(final Composite parent) { ((GridLayout) parent.getLayout()).numColumns += 2; this.load = Buttons.createCentering(parent, "Load", new WidgetListener() { public void handleEvent(final Event event) { performLoad(); } }); this.unload = Buttons.createCentering(parent, "Unload", new WidgetListener() { public void handleEvent(final Event event) { performUnload(); } }); this.registerServiceStateListeners(); this.updateButtons(); } static String environmentText() { SmartStringBuilder text = new SmartStringBuilder(); IProduct product = Platform.getProduct(); text.format("%s %s%n", StandardSystemProperty.JAVA_VM_NAME.value(), Environment.getJavaVersion()); text.format("%s %s%n", product.getName(), Products.getVersion(product)); text.format("PerConIK Core %s%n", defaultInstance().getBundle().getVersion()); text.format("Debug plug-ins %s%n", Environment.debug ? "enabled" : "disabled"); return text.toString(); } static Optional<ResourceService> resourceService() { try { return of(Services.getResourceService()); } catch (UnsupportedOperationException e) { return absent(); } } static Optional<ListenerService> listenerService() { try { return of(Services.getListenerService()); } catch (UnsupportedOperationException e) { return absent(); } } static abstract class ServiceStateListener<S extends Service> extends ServiceListener { final S service; ServiceStateListener(final S service) { this.service = checkNotNull(service); } } void registerServiceStateListeners() { Optional<ResourceService> resourceService = resourceService(); Optional<ListenerService> listenerService = listenerService(); if (resourceService.isPresent()) { ResourceService service = resourceService.get(); service.addListener(new ServiceStateListener<ResourceService>(service) { @Override protected void transit(final State from, final State to, @Nullable final Throwable failure) { Optional<ResourceService> service = resourceService(); if (service.isPresent() && identityHashCode(service.get()) == identityHashCode(this.service)) { setResourceTransition(from, to); sleepUninterruptibly(stateTransitionDisplayPause.duration(), stateTransitionDisplayPause.unit()); } } }, directExecutor()); } if (listenerService.isPresent()) { ListenerService service = listenerService.get(); service.addListener(new ServiceStateListener<ListenerService>(service) { @Override protected void transit(final State from, final State to, @Nullable final Throwable failure) { Optional<ListenerService> service = listenerService(); if (service.isPresent() && identityHashCode(service.get()) == identityHashCode(this.service)) { setListenerTransition(from, to); sleepUninterruptibly(stateTransitionDisplayPause.duration(), stateTransitionDisplayPause.unit()); } } }, directExecutor()); } } void unregisterServiceStateListeners() {} void updatePage() { this.updateMessage(); this.updateStates(); this.updateButtons(); } void updateMessage() { if (loadedServices()) { this.setErrorMessage(null); } else { this.setErrorMessage("Core services not loaded"); } } void updateStates() { this.setResourceState(resourceService()); this.setListenerState(listenerService()); } void updateButtons() { boolean loaded = loadedServices(); this.load.setEnabled(!loaded); this.unload.setEnabled(loaded); this.resourceButton.setEnabled(loaded); this.listenerButton.setEnabled(loaded); } void performLoad() { final Map<String, Map<String, Nameable>> snapshot = ComponentsReporter.snapshot(); try { checkState(loadedServices() == false, "Services already loaded"); this.load.setEnabled(false); loadServices(new Runnable() { public void run() { registerServiceStateListeners(); } }, loadServicesTimeout); awaitServices(awaitServicesTimeout); } catch (TimeoutException failure) { this.handleTimeout(failure, "loading", snapshot); } catch (RuntimeException failure) { this.handleFailure(failure, "loading", snapshot); } this.updatePage(); } void performUnload() { final Map<String, Map<String, Nameable>> snapshot = ComponentsReporter.snapshot(); try { checkState(loadedServices() == true, "Services already unloaded"); this.unload.setEnabled(false); unloadServices(new Runnable() { public void run() { unregisterServiceStateListeners(); } }, unloadServicesTimeout); } catch (TimeoutException failure) { this.handleTimeout(failure, "unloading", snapshot); } catch (Throwable failure) { this.handleFailure(failure, "unloading", snapshot); } this.updatePage(); } private void handleTimeout(final TimeoutException failure, final String action, final Map<String, Map<String, Nameable>> before) { final Map<String, Map<String, Nameable>> after = ComponentsReporter.snapshot(); String title = "Core Services"; String message = format("Unexpected timeout while %s services.", action); String description = format("%s%n%n%s", message, ComponentsReporter.message(action, before, after)); openError(this.getShell(), title, message + " See error log for more details."); Activator.defaultInstance().getConsole().error(failure, description); } private void handleFailure(final Throwable failure, final String action, final Map<String, Map<String, Nameable>> before) { final Map<String, Map<String, Nameable>> after = ComponentsReporter.snapshot(); String title = "Core Services"; String message = firstNonNullOrEmpty(failure.getMessage(), "Unexpected failure") + "."; String description = format("%s%n%n%s", message, ComponentsReporter.message(action, before, after)); openError(this.getShell(), title, message + " See error log for more details."); Activator.defaultInstance().getConsole().error(failure, description); } static final class ComponentsReporter { private ComponentsReporter() {} static Map<String, Map<String, Nameable>> snapshot() { ImmutableMap.Builder<String, Map<String, Nameable>> builder = ImmutableMap.builder(); builder.put("resource", toResourceComponents(resourceService())); builder.put("listener", toListenerComponents(listenerService())); return builder.build(); } static <K> String message(final String action, final Map<?, Map<K, Nameable>> before, final Map<?, Map<K, Nameable>> after) { SmartStringBuilder builder = new SmartStringBuilder().format("snapshot of service components during %s:", action).appendln(); builder.tab().appendln("before:").tab().lines(toString(before)).untab(2); builder.tab().appendln("after:").tab().lines(toString(after)).untab(2); return builder.toString(); } private static <K> String toString(final Map<?, Map<K, Nameable>> snapshot) { SmartStringBuilder builder = new SmartStringBuilder(); for (Entry<?, Map<K, Nameable>> entry: snapshot.entrySet()) { builder.append(entry.getKey()).appendln(":").tab(); builder.lines(toString(entry.getValue().entrySet())).untab(); } return builder.toString(); } private static <K> String toString(final Set<Entry<K, Nameable>> components) { if (components.isEmpty()) { return format("null%n"); } SmartStringBuilder builder = new SmartStringBuilder(); for (Entry<?, Nameable> entry: components) { Nameable component = entry.getValue(); builder.append(entry.getKey()).appendln(":").tab(); builder.append("implementation: ").appendln(component.getClass().getName()); builder.append("name: ").appendln(component.getName()); builder.append("hash: ").appendln(toHexString(component.hashCode())); builder.append("identity: ").appendln(toHexString(identityHashCode(component))); if (component instanceof Service) { builder.append("state: ").appendln(toLowerCase(((Service) component).state())); } builder.untab(); } return builder.toString(); } } static final class DetailsDialog extends StatusDialog { Map<String, Nameable> components; CheckboxTableViewer tableViewer; DetailsDialog(final Shell parent) { super(parent); } @Override protected Control createDialogArea(final Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridLayout parentLayout = new GridLayout(); parentLayout.numColumns = 1; parentLayout.marginHeight = 5; parentLayout.marginWidth = 5; composite.setLayout(parentLayout); composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); Composite tableComposite = new Composite(parent, SWT.NONE); TableColumnLayout tableLayout = new TableColumnLayout(); GridData tableGrid = new GridData(GridData.FILL_BOTH); tableGrid.widthHint = 360; tableGrid.heightHint = this.convertHeightInCharsToPixels(10); tableComposite.setLayout(tableLayout); tableComposite.setLayoutData(tableGrid); Table table = Tables.create(tableComposite, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); GC gc = new GC(this.getShell()); gc.setFont(JFaceResources.getDialogFont()); TableColumns.create(table, tableLayout, "Component", gc, 1); TableColumns.create(table, tableLayout, "Implementation", gc, 4); TableColumns.create(table, tableLayout, "Name", gc, 4); TableColumns.create(table, tableLayout, "Hash", gc, 1); TableColumns.create(table, tableLayout, "Identity", gc, 1); gc.dispose(); this.tableViewer = new RegularTableViewer(table); this.tableViewer.setComparer(ElementComparers.fromEquivalence(rawOptionType(), optionEquivalence())); this.tableViewer.setContentProvider(new MapContentProvider()); this.tableViewer.setLabelProvider(new ComponentLabelProvider()); this.updateTable(); Dialog.applyDialogFont(composite); return composite; } final class ComponentLabelProvider extends LabelProvider implements ITableLabelProvider { ComponentLabelProvider() {} public String getColumnText(final Object element, final int column) { Entry<?, ?> entry = (Entry<?, ?>) element; String type = entry.getKey().toString(); Nameable object = (Nameable) entry.getValue(); switch (column) { case 0: return type; case 1: return object.getClass().getName(); case 2: return object.getName(); case 3: return toHexString(object.hashCode()); case 4: return toHexString(identityHashCode(object)); default: throw new IllegalStateException(); } } public Image getColumnImage(final Object element, final int columnIndex) { return null; } } void updateTable() { this.tableViewer.setInput(this.components); this.tableViewer.refresh(); } void setComponents(final Map<String, Nameable> components) { this.components = checkNotNull(components); } @Override protected IDialogSettings getDialogBoundsSettings() { return DialogSettings.getOrCreateSection(Activator.defaultInstance().getDialogSettings(), DetailsDialog.class.getName()); } @Override public boolean isHelpAvailable() { return false; } @Override protected boolean isResizable() { return true; } } static Map<String, Nameable> toResourceComponents(final Optional<? extends ResourceService> service) { if (service.isPresent()) { return toResourceComponents(service.get()); } return emptyMap(); } static Map<String, Nameable> toResourceComponents(final ResourceService service) { Map<String, Nameable> components = newHashMap(); components.put("service", service); if (service.state() == State.RUNNING) { components.put("provider", service.getResourceProvider()); components.put("manager", service.getResourceManager()); } return components; } static Map<String, Nameable> toListenerComponents(final Optional<? extends ListenerService> service) { if (service.isPresent()) { return toListenerComponents(service.get()); } return emptyMap(); } static Map<String, Nameable> toListenerComponents(final ListenerService service) { Map<String, Nameable> components = newHashMap(); components.put("service", service); if (service.state() == State.RUNNING) { components.put("provider", service.getListenerProvider()); components.put("manager", service.getListenerManager()); } return components; } static String toState(final Optional<? extends Service> service) { return service.isPresent() ? toState(service.get()) : "Unresolved setup."; } static String toState(final Service service) { boolean loaded = loadedServices(); State state = service.state(); return format("%s and %s%s", loaded ? "Loaded" : "Unloaded", toLowerCase(state), terminalStates.contains(state) ? '.' : '\u2026'); } static String toTransition(final State from, final State to) { boolean loaded = loadedServices(); return format("%s and in transition from %s to %s…", loaded ? "Loaded" : "Unloaded", toLowerCase(from), toLowerCase(to)); } void setResourceState(final Optional<ResourceService> service) { if (!this.resourceLabel.isDisposed()) { this.resourceLabel.setText(toState(service)); } } void setListenerState(final Optional<ListenerService> service) { if (!this.listenerLabel.isDisposed()) { this.listenerLabel.setText(toState(service)); } } void setResourceTransition(final State from, final State to) { if (!this.resourceLabel.isDisposed()) { this.resourceLabel.setText(toTransition(from, to)); } } void setListenerTransition(final State from, final State to) { if (!this.listenerLabel.isDisposed()) { this.listenerLabel.setText(toTransition(from, to)); } } @Override public Control getControl() { if (this.isContentCreated()) { this.updatePage(); } return super.getControl(); } }