Java tutorial
/* * Copyright 2015-2016 Red Hat, Inc, and individual contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.hal.core.modelbrowser; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Stack; import javax.inject.Inject; import javax.inject.Provider; import com.google.common.collect.Sets; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.web.bindery.event.shared.EventBus; import elemental2.dom.HTMLButtonElement; import elemental2.dom.HTMLElement; import org.jboss.gwt.elemento.core.Elements; import org.jboss.gwt.elemento.core.IsElement; import org.jboss.hal.ballroom.LabelBuilder; import org.jboss.hal.ballroom.form.Form; import org.jboss.hal.ballroom.form.Form.FinishReset; import org.jboss.hal.ballroom.tree.Node; import org.jboss.hal.ballroom.tree.SelectionContext; import org.jboss.hal.ballroom.tree.Tree; import org.jboss.hal.ballroom.wizard.Wizard; import org.jboss.hal.config.Environment; import org.jboss.hal.core.CrudOperations; import org.jboss.hal.core.mbui.dialog.AddResourceDialog; import org.jboss.hal.core.mbui.dialog.NameItem; import org.jboss.hal.core.mbui.form.ModelNodeForm; import org.jboss.hal.dmr.ModelNode; import org.jboss.hal.dmr.ModelNodeHelper; import org.jboss.hal.dmr.Operation; import org.jboss.hal.dmr.ResourceAddress; import org.jboss.hal.dmr.dispatch.Dispatcher; import org.jboss.hal.flow.FlowContext; import org.jboss.hal.flow.Outcome; import org.jboss.hal.flow.Progress; import org.jboss.hal.flow.Task; import org.jboss.hal.meta.AddressTemplate; import org.jboss.hal.meta.Metadata; import org.jboss.hal.meta.processing.MetadataProcessor; import org.jboss.hal.meta.processing.SuccessfulMetadataCallback; import org.jboss.hal.resources.CSS; import org.jboss.hal.resources.Ids; import org.jboss.hal.resources.Names; import org.jboss.hal.resources.Resources; import org.jboss.hal.spi.Footer; import org.jboss.hal.spi.Message; import org.jboss.hal.spi.MessageEvent; import org.jetbrains.annotations.NonNls; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Completable; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.toList; import static org.jboss.gwt.elemento.core.Elements.*; import static org.jboss.gwt.elemento.core.EventType.click; import static org.jboss.hal.ballroom.LayoutBuilder.column; import static org.jboss.hal.ballroom.LayoutBuilder.row; import static org.jboss.hal.ballroom.Skeleton.MARGIN_BIG; import static org.jboss.hal.ballroom.Skeleton.MARGIN_SMALL; import static org.jboss.hal.ballroom.Skeleton.applicationOffset; import static org.jboss.hal.core.modelbrowser.SingletonState.CHOOSE; import static org.jboss.hal.core.modelbrowser.SingletonState.CREATE; import static org.jboss.hal.dmr.ModelDescriptionConstants.ADD; import static org.jboss.hal.dmr.ModelDescriptionConstants.PROFILE; import static org.jboss.hal.dmr.ModelDescriptionConstants.READ_RESOURCE_OPERATION; import static org.jboss.hal.dmr.ModelDescriptionConstants.SERVER_GROUP; import static org.jboss.hal.flow.Flow.series; import static org.jboss.hal.meta.StatementContext.Tuple.SELECTED_GROUP; import static org.jboss.hal.meta.StatementContext.Tuple.SELECTED_PROFILE; import static org.jboss.hal.resources.CSS.*; import static org.jboss.hal.resources.Ids.MODEL_BROWSER_ROOT; /** Model browser element which can be embedded in other elements. */ public class ModelBrowser implements IsElement<HTMLElement> { @NonNls private static final Logger logger = LoggerFactory.getLogger(ModelBrowser.class); static final HTMLElement PLACE_HOLDER_ELEMENT = div().get(); private final CrudOperations crud; private MetadataProcessor metadataProcessor; private Provider<Progress> progress; private final Dispatcher dispatcher; private final EventBus eventBus; private final Resources resources; private final Stack<FilterInfo> filterStack; private final HTMLElement root; private final HTMLElement buttonGroup; private final HTMLButtonElement filter; private final HTMLButtonElement refresh; private final HTMLButtonElement collapse; private final HTMLElement treeContainer; private final HTMLElement content; private final ResourcePanel resourcePanel; private final ChildrenPanel childrenPanel; Tree<Context> tree; private boolean updateBreadcrumb; private int surroundingHeight; // ------------------------------------------------------ ui setup @Inject public ModelBrowser(CrudOperations crud, MetadataProcessor metadataProcessor, @Footer Provider<Progress> progress, Dispatcher dispatcher, Environment environment, EventBus eventBus, Resources resources) { this.crud = crud; this.metadataProcessor = metadataProcessor; this.progress = progress; this.dispatcher = dispatcher; this.eventBus = eventBus; this.resources = resources; this.filterStack = new Stack<>(); this.updateBreadcrumb = false; this.surroundingHeight = 0; buttonGroup = div().css(btnGroup, modelBrowserButtons) .add(filter = button().css(btn, btnDefault).on(click, event -> filter(tree.getSelected())) .title(resources.constants().filter()).add(i().css(fontAwesome(CSS.filter))).get()) .add(refresh = button().css(btn, btnDefault).on(click, event -> refresh(tree.getSelected())) .title(resources.constants().refresh()).add(i().css(fontAwesome(CSS.refresh))).get()) .add(collapse = button().css(btn, btnDefault).on(click, event -> collapse(tree.getSelected())) .title(resources.constants().collapse()).add(i().css(fontAwesome("minus"))).get()) .get(); treeContainer = div().css(CSS.treeContainer).get(); content = div().css(modelBrowserContent).get(); resourcePanel = new ResourcePanel(this, dispatcher, resources); for (HTMLElement element : resourcePanel) { content.appendChild(element); } resourcePanel.hide(); childrenPanel = new ChildrenPanel(this, environment, dispatcher, metadataProcessor, resources); for (HTMLElement element : childrenPanel) { content.appendChild(element); } childrenPanel.hide(); root = row().add(column(4).addAll(buttonGroup, treeContainer)).add(column(8).add(content)).get(); } private void adjustHeight() { int buttonGroup = (int) this.buttonGroup.offsetHeight; int treeContainerOffset = applicationOffset() + 2 * MARGIN_BIG + buttonGroup + MARGIN_SMALL + surroundingHeight; int contentOffset = applicationOffset() + 2 * MARGIN_BIG + surroundingHeight; treeContainer.style.height = vh(treeContainerOffset); content.style.height = vh(contentOffset); } private void initTree(ResourceAddress address, String text) { Context context = new Context(address, Collections.emptySet()); Node<Context> rootNode = new Node.Builder<>(MODEL_BROWSER_ROOT, text, context).asyncFolder().build(); tree = new Tree<>(Ids.MODEL_BROWSER, rootNode, new ReadChildren(dispatcher)); Elements.removeChildrenFrom(treeContainer); treeContainer.appendChild(tree.element()); tree.attach(); tree.onSelectionChange((event, selectionContext) -> onTreeSelection(selectionContext)); childrenPanel.attach(); } @SuppressWarnings("unchecked") private void emptyTree() { Context context = new Context(ResourceAddress.root(), Collections.emptySet()); Node<Context> rootNode = new Node.Builder<>(MODEL_BROWSER_ROOT, Names.NOT_AVAILABLE, context).asyncFolder() .build(); tree = new Tree<>(Ids.MODEL_BROWSER, rootNode, (node, callback) -> callback.result(new Node[0])); Elements.removeChildrenFrom(treeContainer); treeContainer.appendChild(tree.element()); tree.attach(); childrenPanel.hide(); resourcePanel.hide(); } // ------------------------------------------------------ event handler & co private void filter(Node<Context> node) { if (node != null && node.parent != null) { Node<Context> parent = tree.getNode(node.parent); FilterInfo filterInfo = new FilterInfo(parent, node); filterStack.add(filterInfo); filter(filterInfo); tree.openNode(MODEL_BROWSER_ROOT, () -> tree.selectNode(MODEL_BROWSER_ROOT)); } } private void filter(FilterInfo filter) { elemental2.dom.Element oldFilterElement = buttonGroup.querySelector("." + tagManagerContainer); if (filter.filterText != null) { HTMLElement filterElement = div().css(tagManagerContainer) .add(span().css(tmTag, tagManagerTag).add(span().textContent(filter.filterText)).add( a().css(clickable, tmTagRemove).on(click, event -> clearFilter()).textContent("x"))) //NON-NLS .get(); if (oldFilterElement != null) { buttonGroup.replaceChild(filterElement, oldFilterElement); } else { buttonGroup.appendChild(filterElement); } } else if (oldFilterElement != null) { buttonGroup.removeChild(oldFilterElement); } // reset tree tree.destroy(); initTree(filter.address, filter.text); } private void clearFilter() { elemental2.dom.Element filterElement = buttonGroup.querySelector("." + tagManagerContainer); if (filterElement != null) { buttonGroup.removeChild(filterElement); } if (!filterStack.isEmpty()) { FilterInfo previousFilter = filterStack.pop(); filter(filterStack.isEmpty() ? FilterInfo.ROOT : filterStack.peek()); List<OpenNodeTask> tasks = previousFilter.parents.stream().map(OpenNodeTask::new).collect(toList()); series(new FlowContext(progress.get()), tasks).subscribe(new Outcome<FlowContext>() { @Override public void onError(FlowContext context, Throwable error) { logger.debug("Failed to restore selection {}", previousFilter.parents); } @Override public void onSuccess(FlowContext context) { tree.selectNode(previousFilter.node.id); } }); } } private void refresh(Node<Context> node) { if (node != null) { updateNode(node); tree.refreshNode(node.id); } } private void collapse(Node<Context> node) { if (node != null) { tree.selectNode(node.id, true); } } private void onTreeSelection(SelectionContext<Context> context) { if ("ready".equals(context.action)) { //NON-NLS // only (de)selection events please return; } filter.disabled = context.selected.length == 0 || !context.node.data.isFullyQualified() || context.node.id.equals(MODEL_BROWSER_ROOT); refresh.disabled = context.selected.length == 0; collapse.disabled = context.selected.length == 0; resourcePanel.hide(); childrenPanel.hide(); if (context.selected.length == 0) { updateBreadcrumb(null); } else { updateNode(context.node); } } private void updateNode(Node<Context> node) { updateBreadcrumb(node); ResourceAddress address = node.data.getAddress(); if (node.data.isFullyQualified()) { showResourceView(node, address); } else { childrenPanel.update(node, address); childrenPanel.show(); } } private void updateBreadcrumb(Node<Context> node) { if (updateBreadcrumb) { ModelBrowserPath path = new ModelBrowserPath(this, node); eventBus.fireEvent(new ModelBrowserPathEvent(path)); } } private void showResourceView(Node<Context> node, ResourceAddress address) { Node<Context> parent = tree.getNode(node.parent); AddressTemplate template = asGenericTemplate(parent, address); metadataProcessor.lookup(template, progress.get(), new SuccessfulMetadataCallback(eventBus, resources) { @Override public void onMetadata(Metadata metadata) { resourcePanel.update(node, node.data.getAddress(), metadata); resourcePanel.show(); } }); } void add(Node<Context> parent, List<String> children) { if (parent.data.hasSingletons()) { if (parent.data.getSingletons().size() == children.size()) { MessageEvent.fire(eventBus, Message.warning(resources.messages().allSingletonsExist())); } else if (parent.data.getSingletons().size() - children.size() == 1) { // no need to show a wizard - find the missing singleton HashSet<String> singletons = Sets.newHashSet(parent.data.getSingletons()); singletons.removeAll(children); String singleton = singletons.iterator().next(); ResourceAddress singletonAddress = parent.data.getAddress().getParent().add(parent.text, singleton); AddressTemplate template = asGenericTemplate(parent, singletonAddress); String id = Ids.build(parent.id, "singleton", Ids.ADD); crud.addSingleton(id, singleton, template, address -> refresh(parent)); } else { // open wizard to choose the singleton Wizard<SingletonContext, SingletonState> wizard = new Wizard.Builder<SingletonContext, SingletonState>( resources.messages().addResourceTitle(parent.text), new SingletonContext(parent, children)) .addStep(CHOOSE, new ChooseSingletonStep(parent, children, resources)) .addStep(CREATE, new CreateSingletonStep(parent, metadataProcessor, progress, eventBus, resources)) .onBack((context, currentState) -> currentState == CREATE ? CHOOSE : null) .onNext((context, currentState) -> currentState == CHOOSE ? CREATE : null) .onFinish((wzrd, context) -> { Operation.Builder builder = new Operation.Builder( fqAddress(parent, context.singleton), ADD); if (context.modelNode != null) { builder.payload(context.modelNode); } dispatcher.execute(builder.build(), result -> { MessageEvent.fire(eventBus, Message.success(resources.messages() .addResourceSuccess(parent.text, context.singleton))); refresh(parent); }); }).build(); wizard.show(); } } else { AddressTemplate template = asGenericTemplate(parent, parent.data.getAddress()); metadataProcessor.lookup(template, progress.get(), new SuccessfulMetadataCallback(eventBus, resources) { @Override public void onMetadata(Metadata metadata) { String title = new LabelBuilder().label(parent.text); NameItem nameItem = new NameItem(); String id = Ids.build(parent.id, "add"); ModelNodeForm<ModelNode> form = new ModelNodeForm.Builder<>(id, metadata) .unboundFormItem(nameItem, 0).fromRequestProperties().panelForOptionalAttributes() .build(); AddResourceDialog dialog = new AddResourceDialog(resources.messages().addResourceTitle(title), form, (name1, model) -> crud.add(title, nameItem.getValue(), fqAddress(parent, nameItem.getValue()), model, (n, a) -> refresh(parent))); dialog.show(); } }); } } static AddressTemplate asGenericTemplate(Node<Context> node, ResourceAddress address) { return AddressTemplate.of(address, (name, value, first, last, index, size) -> { String segment; if (PROFILE.equals(name)) { segment = SELECTED_PROFILE.variable(); } else if (SERVER_GROUP.equals(name)) { segment = SELECTED_GROUP.variable(); } else { if (last && node != null && node.data != null && !node.data.hasSingletons()) { segment = name + "=*"; } else { segment = name + "=" + ModelNodeHelper.encodeValue(value); } } return segment; }); } private ResourceAddress fqAddress(Node<Context> parent, String child) { return parent.data.getAddress().getParent().add(parent.text, child); } void remove(ResourceAddress address) { crud.remove(address.lastName(), address.lastValue(), address, () -> refresh(tree.getSelected())); } void save(ResourceAddress address, Map<String, Object> changedValues, Metadata metadata) { crud.save(address.lastName(), address.lastValue(), address, changedValues, metadata, () -> refresh(tree.getSelected())); } void reset(ResourceAddress address, Form<ModelNode> form, Metadata metadata) { crud.reset(address.lastName(), address.lastValue(), address, form, metadata, new FinishReset<ModelNode>(form) { @Override public void afterReset(Form<ModelNode> form) { refresh(tree.getSelected()); } }); } // ------------------------------------------------------ public API /** * Use this method if you embed the model browser into an application view and if you have additional elements * before or after the model browser. This method should be called when the application view is attached or before * {@link #setRoot(ResourceAddress, boolean)} is called. * * @param surroundingHeight the sum of the height of all surrounding elements */ public void setSurroundingHeight(int surroundingHeight) { this.surroundingHeight = surroundingHeight; adjustHeight(); } /** * Entry point to show the specified address. * * @param root the root address for this model browser * @param updateBreadcrumb {@code true} if this model browser should fire {@link ModelBrowserPathEvent}s */ public void setRoot(ResourceAddress root, boolean updateBreadcrumb) { this.updateBreadcrumb = updateBreadcrumb; String resource = root.equals(ResourceAddress.root()) ? Names.MANAGEMENT_MODEL : SafeHtmlUtils.htmlEscapeAllowEntities(root.lastValue()); if ("*".equals(resource)) { throw new IllegalArgumentException("Invalid root address: " + root + ". ModelBrowser.setRoot() must be called with a concrete address."); } // TODO Removing a filter in a scoped model browser does not work Elements.setVisible(filter, root.equals(ResourceAddress.root())); Operation ping = new Operation.Builder(root, READ_RESOURCE_OPERATION).build(); dispatcher.execute(ping, result -> { initTree(root, resource); tree.openNode(MODEL_BROWSER_ROOT, () -> resourcePanel.tabs.showTab(0)); tree.selectNode(MODEL_BROWSER_ROOT); adjustHeight(); }, (operation, failure) -> { emptyTree(); MessageEvent.fire(eventBus, Message.error(resources.messages().unknownResource(), resources.messages().unknownResourceDetails(root.toString(), failure))); adjustHeight(); }, (operation, exception) -> { emptyTree(); MessageEvent.fire(eventBus, Message.error(resources.messages().unknownResource(), resources.messages().unknownResourceDetails(root.toString(), exception.getMessage()))); adjustHeight(); }); } public void select(String id, boolean closeSelected) { tree.selectNode(id, closeSelected); } @Override public HTMLElement element() { return root; } private static class FilterInfo { static final FilterInfo ROOT = new FilterInfo(null, null); final ResourceAddress address; final Node<Context> node; final String text; final String filterText; final List<String> parents; private FilterInfo(Node<Context> parent, Node<Context> child) { this.address = child == null ? ResourceAddress.root() : child.data.getAddress(); this.node = child; this.text = child == null ? Names.MANAGEMENT_MODEL : child.text; this.filterText = parent == null || child == null ? null : parent.text + "=" + child.text; this.parents = child == null ? emptyList() : asList(child.parents); if (!parents.isEmpty()) { Collections.reverse(parents); parents.remove(0); // get rif of the artificial root } } } private class OpenNodeTask implements Task<FlowContext> { private final String id; private OpenNodeTask(String id) { this.id = id; } @Override public Completable call(FlowContext context) { return Completable.fromEmitter(emitter -> { if (tree.getNode(id) != null) { tree.openNode(id, emitter::onCompleted); } else { emitter.onCompleted(); } }); } } }