/******************************************************************************* * Copyright (c) 2012 - 2016 GoPivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * GoPivotal, Inc. - initial API and implementation * DISID Corporation, S.L - Spring Roo maintainer *******************************************************************************/ package org.springframework.ide.eclipse.roo.ui.internal; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.fieldassist.ContentProposalAdapter; import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.fieldassist.FieldDecoration; import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalListener; import org.eclipse.jface.fieldassist.IContentProposalProvider; import org.eclipse.jface.fieldassist.TextContentAdapter; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.KeyListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.model.WorkbenchLabelProvider; import org.springframework.ide.eclipse.core.SpringCore; import org.springframework.ide.eclipse.roo.core.RooCoreActivator; import org.springframework.ide.eclipse.roo.core.model.IRooInstall; import org.springframework.ide.eclipse.roo.ui.RooUiActivator; import org.springframework.roo.shell.eclipse.Bootstrap; import org.springframework.roo.shell.eclipse.ProjectRefresher; import org.springframework.util.StringUtils; import org.springsource.ide.eclipse.commons.core.CommandHistoryProvider; import org.springsource.ide.eclipse.commons.core.Entry; import org.springsource.ide.eclipse.commons.core.ICommandHistory; import org.springsource.ide.eclipse.commons.frameworks.core.internal.commands.ICommandListener; import org.springsource.ide.eclipse.commons.ui.CommandHistoryPopupList; import org.springsource.ide.eclipse.commons.ui.SpringUIUtils; /** * A single shell instance. * @author Christian Dupuis * @author Steffen Pingel * @author Kris De Volder * @author Juan Carlos GarcĂ­a * @since 2.5.0 */ public class RooShellTab { private static final CommandHistoryPopupList.LabelProvider historyLabelProvider = new CommandHistoryPopupList.LabelProvider() { @Override public String getLabel(Entry entry) { return entry.getCommand(); } }; private static Map<String, String> PATH_MAPPING; private StyledTextAppender appender; private Bootstrap bootstrap; private Text command; private final ICommandHistory history = CommandHistoryProvider.getCommandHistory(RooUiActivator.PLUGIN_ID, RooCoreActivator.NATURE_ID); private String initialCommand; private boolean isReady = false; private String lastCompletionProposal = null; private StyledText text; protected IRooInstall install; protected final IProject project; protected final RooShellView shellView; /** * If not null, the shell has been initialized. */ protected volatile IStatus initializationStatus; private enum State { CREATED, INITIALIZING, INITIALIZED }; private volatile State state = State.CREATED; static { PATH_MAPPING = new HashMap<String, String>(); PATH_MAPPING.put("SRC_MAIN_JAVA", "src/main/java"); PATH_MAPPING.put("SRC_MAIN_RESOURCES", "src/main/resources"); PATH_MAPPING.put("SRC_MAIN_WEBAPP", "src/main/webapp"); PATH_MAPPING.put("SRC_TEST_JAVA", "src/test/java"); PATH_MAPPING.put("SRC_TEST_RESOURCES", "src/test/resources"); PATH_MAPPING.put("SPRING_CONFIG_ROOT", "src/main/resources/META-INF/spring"); PATH_MAPPING.put("ROOT", ""); } public RooShellTab(IProject project, String initialCommand, RooShellView shellView) { this.project = project; this.shellView = shellView; this.initialCommand = initialCommand; } public void addCommands(ICommandListener listener) { new WizardCommandJob(listener); } public CTabItem addTab(CTabFolder parent) { final Composite tabComposite = new Composite(parent, SWT.NONE); tabComposite.setFont(parent.getFont()); GridLayout layout = new GridLayout(1, false); if (Platform.getOS().equals(Platform.OS_MACOSX) && Platform.getWS().equals(Platform.WS_CARBON)) { layout.marginLeft = -9; layout.marginRight = -9; layout.marginTop = -9; layout.marginBottom = -9; layout.horizontalSpacing = -10; layout.verticalSpacing = -10; } else { layout.marginWidth = 0; layout.marginHeight = 0; layout.horizontalSpacing = 0; layout.verticalSpacing = 0; } tabComposite.setLayout(layout); tabComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); text = new StyledText(tabComposite, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL | SWT.WRAP); GridData data = new GridData(GridData.FILL_BOTH); text.setLayoutData(data); text.setFont(JFaceResources.getTextFont()); text.setEditable(false); RooUiColors.applyShellBackground(text); RooUiColors.applyShellForeground(text); RooUiColors.applyShellFont(text); text.setText("Please stand by until the Roo Shell is completely loaded.\n\r"); text.addListener(SWT.MouseUp, new Listener() { public void handleEvent(Event event) { handleMouseUp(event); } }); new Label(tabComposite, SWT.NONE); Composite commandComposite = new Composite(tabComposite, SWT.NONE); layout = new GridLayout(2, false); layout.marginLeft = 0; layout.marginRight = 0; layout.horizontalSpacing = 0; layout.verticalSpacing = 0; commandComposite.setLayout(layout); commandComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); Label label = new Label(commandComposite, SWT.NONE); label.setText("roo> "); command = new Text(commandComposite, SWT.BORDER); data = new GridData(GridData.FILL_HORIZONTAL); data.horizontalIndent = 5; command.setLayoutData(data); addTypeFieldAssistToText(command); command.addKeyListener(new KeyListener() { public void keyPressed(KeyEvent e) { processKeyEvent(text, e, command.getText()); } public void keyReleased(KeyEvent e) { } }); CTabItem item = new CTabItem(parent, SWT.CLOSE); item.setText(project.getName()); item.setImage(new WorkbenchLabelProvider().getImage(project)); item.setControl(tabComposite); item.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { shellView.removeTab(project); tabComposite.dispose(); removeTab(); } }); parent.showItem(item); parent.setSelection(parent.indexOf(item)); setEnabled(false); setupBootstrap(); return item; } public void addTypeFieldAssistToText(final Text text) { int bits = SWT.TOP | SWT.LEFT; ControlDecoration controlDecoration = new ControlDecoration(text, bits); controlDecoration.setMarginWidth(0); controlDecoration.setShowHover(true); controlDecoration.setShowOnlyOnFocus(true); FieldDecoration contentProposalImage = FieldDecorationRegistry.getDefault().getFieldDecoration( FieldDecorationRegistry.DEC_CONTENT_PROPOSAL); controlDecoration.setImage(contentProposalImage.getImage()); // Create the proposal provider RooShellProposalProvider proposalProvider = new RooShellProposalProvider(text); TextContentAdapter textContentAdapter = new TextContentAdapter(); final RooContentProposalAdapter adapter = new RooContentProposalAdapter(text, textContentAdapter, proposalProvider, KeyStroke.getInstance(SWT.CTRL, SWT.SPACE), null); ILabelProvider labelProvider = new LabelProvider(); adapter.setLabelProvider(labelProvider); adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); adapter.setFilterStyle(ContentProposalAdapter.FILTER_NONE); adapter.addContentProposalListener(new IContentProposalListener() { public void proposalAccepted(IContentProposal proposal) { lastCompletionProposal = proposal.getContent(); } }); text.addControlListener(new ControlAdapter() { public void controlResized(ControlEvent e) { adapter.setPopupSize(new Point(text.getBounds().width, 200)); } }); } public StyledText getText() { return text; } public StyledTextAppender getStyledTextAppender() { return appender; } public boolean isReady() { return this.isReady; } public void removeTab() { if (bootstrap != null) { final Bootstrap shutdownBootstrap = bootstrap; Job shutdownJob = new Job("Shutdown Roo") { @Override protected IStatus run(IProgressMonitor monitor) { shutdownBootstrap.shutdown(); return Status.OK_STATUS; } }; shutdownJob.schedule(); } bootstrap = null; } public void executeCommand(String command) { new CommandJob(command); history.add(new Entry(command, project.getName())); } private Entry commandHistoryPopup(Control showBelow, Entry[] entries) { if (entries.length > 1) { CommandHistoryPopupList popup = new CommandHistoryPopupList(showBelow.getShell()); popup.setLabelProvider(historyLabelProvider); popup.setItems(entries); return popup.open(showBelow.getDisplay().map(showBelow.getParent(), null, showBelow.getBounds())); } else if (entries.length == 0) { return null; } else { return entries[0]; } } private void commandHistoryPopup(Text commandText) { List<Entry> entries = history.getRecentValid(ICommandHistory.DEFAULT_MAX_SIZE); List<Entry> filteredEntries = new ArrayList<Entry>(); int counter = 0; // filter out empty commands and those that don't start with the given // string for (Entry entry : entries) { if (counter <= 20 && entry.getCommand() != null && entry.getCommand().length() > 0) { if (entry.getCommand().startsWith(commandText.getText())) { filteredEntries.add(entry); counter++; } } } Entry chosen = commandHistoryPopup(commandText, filteredEntries.toArray(new Entry[filteredEntries.size()])); if (chosen != null) { commandText.setText(chosen.getCommand()); commandText.setSelection(chosen.getCommand().length()); } } private void handleMouseUp(Event event) { int offset = text.getCaretOffset(); StyleRange range = offset > 0 ? text.getStyleRangeAtOffset(offset - 1) : null; if (range != null) { Object data = StyledTextAppender.getData(range); if (data instanceof String) { String fileName = (String) data; int ix = fileName.indexOf('/'); String newFileName = ""; int pipe = fileName.indexOf('|'); if (pipe > -1) { ix = fileName.indexOf('/', pipe); String folder = fileName.substring(pipe + 1, ix); newFileName = StringUtils.replace(fileName, folder, PATH_MAPPING.get(folder)); newFileName = StringUtils.replace(newFileName, "|", "/"); } else { String folder = fileName.substring(0, ix); newFileName = StringUtils.replace(fileName, folder, PATH_MAPPING.get(folder)); } IResource resource = project.findMember(newFileName); if (resource instanceof IFile) { SpringUIUtils.openInEditor((IFile) resource, -1); } } } } private void setEnabled(final boolean enable) { Display.getDefault().asyncExec(new Runnable() { public void run() { if (!text.isDisposed()) { text.setEnabled(enable); } if (!command.isDisposed()) { command.setEnabled(enable); } RooShellTab.this.isReady = enable; } }); } private void setupBootstrap() { Job setupJob = new Job("Opening Roo Shell for project '" + project.getName() + "'") { @Override protected IStatus run(IProgressMonitor monitor) { install = RooCoreActivator.getDefault().getInstallManager().getRooInstall(project); appender = new StyledTextAppender(text); if (install == null) { final Status status = new Status( IStatus.ERROR, RooUiActivator.PLUGIN_ID, "No valid Spring Roo installation configured. Use the 'Roo Support' preference pane to configure available Roo installations."); Display.getDefault().asyncExec(new Runnable() { public void run() { appender.append(status.getMessage(), Level.SEVERE.intValue()); } }); initializationStatus = status; return Status.OK_STATUS; } final IStatus status = install.validate(); if (!status.isOK()) { Display.getDefault().asyncExec(new Runnable() { public void run() { appender.append(status.getMessage(), Level.SEVERE.intValue()); } }); initializationStatus = status; return Status.OK_STATUS; } String projectLocation = null; if (project.getLocation() != null) { projectLocation = project.getLocation().toOSString(); } else if (project.getRawLocation() != null) { projectLocation = project.getRawLocation().toOSString(); } try { ProjectRefresher refresher = new ProjectRefresher(project); bootstrap = new Bootstrap(project, projectLocation, install.getHome(), install.getVersion(), refresher); bootstrap.start(appender, project.getName()); setEnabled(true); // refresh the project to make sure we are in sync with Roo refresher.refresh(null, false); if (initialCommand != null) { new CommandJob(initialCommand); } initializationStatus = Status.OK_STATUS; } catch (Throwable e) { SpringCore.log(e); initializationStatus = new Status(Status.ERROR, RooCoreActivator.PLUGIN_ID, e.getMessage(), e); } return Status.OK_STATUS; } }; setupJob.addJobChangeListener(new JobChangeAdapter() { @Override public void done(IJobChangeEvent event) { state = State.INITIALIZED; } }); setupJob.setPriority(Job.INTERACTIVE); this.state = State.INITIALIZING; setupJob.schedule(); } public IStatus waitForInitialization() { try { PlatformUI.getWorkbench().getProgressService().busyCursorWhile(new IRunnableWithProgress() { public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { try { monitor.beginTask("Initializing Roo", IProgressMonitor.UNKNOWN); waitForInitialization(monitor); } finally { monitor.done(); } } }); return initializationStatus; } catch (InvocationTargetException e) { Status status = new Status(IStatus.ERROR, RooUiActivator.PLUGIN_ID, "Unexpected error during Roo initialization", e); RooUiActivator.getDefault().getLog().log(status); return status; } catch (InterruptedException e) { return Status.CANCEL_STATUS; } } public IStatus waitForInitialization(IProgressMonitor monitor) throws InterruptedException { if (state != State.INITIALIZING && state != State.INITIALIZED) { throw new IllegalStateException("Invoke addTab() first"); } if (monitor.isCanceled()) { throw new InterruptedException(); } while (initializationStatus == null) { try { Thread.sleep(200); } catch (InterruptedException e) { // ignore } if (monitor.isCanceled()) { throw new InterruptedException(); } } return initializationStatus; } protected void processKeyEvent(final StyledText text, KeyEvent e, final String commandString) { Text sender = (Text) e.widget; if (e.character == SWT.CR || e.character == SWT.LF) { if (lastCompletionProposal == null || !lastCompletionProposal.equals(commandString)) { text.setTopIndex(text.getLineCount() - 1); executeCommand(commandString); command.setText(""); e.doit = true; } else { lastCompletionProposal = null; e.doit = false; } } else if (e.keyCode == SWT.ARROW_UP) { commandHistoryPopup(sender); e.doit = false; } else if (e.keyCode == SWT.ARROW_DOWN) { commandHistoryPopup(sender); e.doit = false; } } private class CommandJob extends Job { private final String commandString; public CommandJob(String commandString) { super("Execute command '" + commandString + "' on project '" + project.getName() + "'"); this.commandString = commandString; setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule()); setPriority(Job.INTERACTIVE); schedule(); } @Override protected IStatus run(IProgressMonitor monitor) { // Wait before we accept the command while (!RooShellTab.this.isReady()) { try { Thread.sleep(1000); } catch (InterruptedException e) { } } boolean isShutdown = false; try { final boolean[] done = { false }; Display.getDefault().asyncExec(new Runnable() { public void run() { if (!appender.hasPrompt()) { appender.append(bootstrap.getShellPrompt() + StyledTextAppender.NL, Level.INFO.intValue()); } appender.append(commandString + StyledTextAppender.NL, Level.ALL.intValue()); done[0] = true; } }); if (bootstrap != null) { // Wait for the command to be sent to the console while (!done[0]) { Thread.sleep(100); } bootstrap.execute(commandString); isShutdown = bootstrap.isShutdown(); } } catch (Throwable ex) { return new Status(Status.ERROR, RooCoreActivator.PLUGIN_ID, ex.getMessage(), ex); } if (isShutdown) { new UiCommands(RooShellTab.this).exit(); } return Status.OK_STATUS; } } private class WizardCommandJob extends Job { private final ICommandListener listener; public WizardCommandJob(ICommandListener listener) { super(""); this.listener = listener; setPriority(Job.INTERACTIVE); setSystem(true); schedule(); } @Override protected IStatus run(IProgressMonitor monitor) { // Wait before we accept the command while (!RooShellTab.this.isReady()) { try { Thread.sleep(1000); } catch (InterruptedException e) { } } bootstrap.addCommand(listener); return Status.OK_STATUS; } } private class RooShellContentProposal implements IContentProposal { private final String fContent; private final String fDescription; private final Image fImage; private final String fLabel; public RooShellContentProposal(String label, String content, String description, Image image) { fLabel = label; fContent = content; fDescription = description; fImage = image; } public String getContent() { return fContent; } public int getCursorPosition() { if (fContent != null) { return fContent.length(); } return 0; } public String getDescription() { return fDescription; } @SuppressWarnings("unused") public Image getImage() { return fImage; } public String getLabel() { return fLabel; } @Override public String toString() { return fLabel; } } private class RooShellProposalProvider implements IContentProposalProvider { private final Text text; public RooShellProposalProvider(Text text) { this.text = text; } public IContentProposal[] getProposals(String contents, int position) { List<IContentProposal> proposals = new ArrayList<IContentProposal>(); List<String> stringProposals = new ArrayList<String>(); Integer pos = 0; String prefix = ""; try { pos = bootstrap.complete(contents, position, stringProposals); } catch (Throwable e) { SpringCore.log(e); } if (pos > 0) { prefix = contents.substring(0, pos); } if (stringProposals.size() == 1) { text.setText(prefix + stringProposals.get(0)); text.setSelection(text.getText().length()); return new IContentProposal[0]; } else { for (String stringProposal : stringProposals) { proposals.add(new RooShellContentProposal(prefix + stringProposal, prefix + stringProposal, findDescription(prefix + stringProposal), null)); } return proposals.toArray(new IContentProposal[proposals.size()]); } } private String findDescription(String proposal) { int i = 0; String description = proposal; for (Map.Entry<String, String> entry : bootstrap.getCommandDescription().entrySet()) { if (proposal.startsWith(entry.getKey()) && entry.getKey().length() >= i) { description = entry.getValue(); i = entry.getKey().length(); } } System.out.println("description = "+description); return description; } } public Bootstrap getBootstrap() { return bootstrap; } }