/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2015 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2015 Sun Microsystems, Inc.
*/
package org.netbeans.modules.php.cake3.commands;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.extexecution.ExecutionDescriptor;
import org.netbeans.api.extexecution.base.input.InputProcessor;
import org.netbeans.api.extexecution.base.input.InputProcessors;
import org.netbeans.api.extexecution.base.input.LineProcessor;
import org.netbeans.modules.php.api.executable.InvalidPhpExecutableException;
import org.netbeans.modules.php.api.executable.PhpExecutable;
import org.netbeans.modules.php.api.executable.PhpExecutableValidator;
import org.netbeans.modules.php.api.phpmodule.PhpModule;
import org.netbeans.modules.php.api.util.UiUtils;
import org.netbeans.modules.php.cake3.modules.CakePHP3Module;
import org.netbeans.modules.php.cake3.modules.CakePHP3Module.Base;
import org.netbeans.modules.php.spi.framework.commands.FrameworkCommand;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.HtmlBrowser;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.openide.windows.InputOutput;
import org.xml.sax.SAXException;
/**
*
* @author junichi11
*/
public final class Cake3Script {
public static final String SCRIPT_NAME = "cake"; // NOI18N
public static final String SCRIPT_NAME_BAT = SCRIPT_NAME + ".bat"; // NOI18N
public static final String SCRIPT_NAME_LONG = SCRIPT_NAME + ".php"; // NOI18N
// commands
private static final String COMMAND_LIST_COMMAND = "command_list"; // NOI18N
private static final String SERVER_COMMAND = "server"; // NOI18N
// params
private static final String HELP_PARAM = "--help"; // NOI18N
private static final String XML_PARAM = "--xml"; // NOI18N
private static final List<String> COMMAND_LIST_XML_COMMAND = Arrays.asList(COMMAND_LIST_COMMAND, XML_PARAM);
private static final List<String> DEFAULT_PARAMS = Collections.emptyList();
private static final Logger LOGGER = Logger.getLogger(Cake3Script.class.getName());
private final String cakePath;
private Cake3Script(String cakePath) {
this.cakePath = cakePath;
}
/**
* Get the project specific, <b>valid only</b> Cake script. If not found,
* {@code null} is returned.
*
* @param phpModule PHP module for which Cake script is taken
* @param warn {@code true} if user is warned when the Cake script is not
* valid
* @return Cake console script or {@code null} if the script is not valid
*/
@NbBundle.Messages({
"# {0} - error message",
"CakeScript.script.invalid=<html>Project's Cake script is not valid.<br>({0})"
})
public static Cake3Script forPhpModule(PhpModule phpModule, boolean warn) throws InvalidPhpExecutableException {
String console = null;
FileObject script = getScript(phpModule);
if (script != null) {
console = FileUtil.toFile(script).getAbsolutePath();
}
String error = validate(console);
if (error == null) {
return new Cake3Script(console);
}
if (warn) {
NotifyDescriptor.Message message = new NotifyDescriptor.Message(
Bundle.CakeScript_script_invalid(error),
NotifyDescriptor.WARNING_MESSAGE);
DialogDisplayer.getDefault().notify(message);
}
throw new InvalidPhpExecutableException(error);
}
private static FileObject getScript(PhpModule phpModule) {
CakePHP3Module module = CakePHP3Module.forPhpModule(phpModule);
if (module == null) {
return null;
}
List<FileObject> directories = module.getDirectories(Base.APP);
String cakeScriptPathFormat = "bin/%s"; // NOI18N
String cakeScriptPath;
if (Utilities.isWindows()) {
// #35 the bat file outputs an empty line
cakeScriptPath = String.format(cakeScriptPathFormat, SCRIPT_NAME_LONG);
} else {
cakeScriptPath = String.format(cakeScriptPathFormat, SCRIPT_NAME);
}
for (FileObject directory : directories) {
return directory.getFileObject(cakeScriptPath);
}
LOGGER.log(Level.WARNING, "Not found {0}", cakeScriptPath); // NOI18N
return null;
}
@NbBundle.Messages("CakeScript.script.label=Cake script")
public static String validate(String command) {
return PhpExecutableValidator.validateCommand(command, Bundle.CakeScript_script_label());
}
/**
* Run built-in server.
*
* @param phpModule PhpModule
*/
public Future<Integer> server(PhpModule phpModule) {
return runCommand(phpModule, Arrays.asList(SERVER_COMMAND), null);
}
public Future<Integer> runCommand(PhpModule phpModule, List<String> parameters, Runnable postExecution) {
LineProcessor lineProcessor = null;
if (parameters.contains(SERVER_COMMAND)) {
lineProcessor = new ServerLineProcessor();
}
return createPhpExecutable(phpModule)
.displayName(getDisplayName(phpModule, parameters.get(0)))
.additionalParameters(getAllParams(parameters))
.run(getDescriptor(postExecution), getOutProcessorFactory(lineProcessor));
}
public String getHelp(PhpModule phpModule, String[] params) {
assert phpModule != null;
List<String> allParams;
allParams = new ArrayList<>();
// #116 cakephp-netbeans
// allParams.addAll(getAppParam(phpModule));
allParams.addAll(Arrays.asList(params));
allParams.add(HELP_PARAM);
HelpLineProcessor lineProcessor = new HelpLineProcessor();
Future<Integer> result = createPhpExecutable(phpModule)
.displayName(getDisplayName(phpModule, allParams.get(0)))
.additionalParameters(getAllParams(allParams))
.run(getSilentDescriptor(), getOutProcessorFactory(lineProcessor));
try {
if (result != null) {
result.get();
}
} catch (CancellationException ex) {
// canceled
} catch (ExecutionException ex) {
UiUtils.processExecutionException(ex, getOptionsPath());
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return lineProcessor.getHelp();
}
public List<FrameworkCommand> getCommands(PhpModule phpModule) {
List<FrameworkCommand> freshCommands = getFrameworkCommandsInternalXml(phpModule);
if (freshCommands != null) {
return freshCommands;
}
// XXX some error => rerun command with console
runCommand(phpModule, Collections.singletonList(COMMAND_LIST_COMMAND), null);
return Collections.emptyList();
}
@NbBundle.Messages({
"Cake3Script.redirect.xml.error=error is occurred when xml file is created for command list."
})
private List<FrameworkCommand> getFrameworkCommandsInternalXml(PhpModule phpModule) {
File tmpFile;
try {
tmpFile = File.createTempFile("nb-cake-commands-", ".xml"); // NOI18N
tmpFile.deleteOnExit();
} catch (IOException ex) {
LOGGER.log(Level.WARNING, null, ex);
return null;
}
// #116 cakephp-netbeans
// List<String> appParam = getAppParam(phpModule);
ArrayList<String> listXmlParams = new ArrayList<>();
// listXmlParams.addAll(appParam);
listXmlParams.addAll(COMMAND_LIST_XML_COMMAND);
if (!redirectToFile(phpModule, tmpFile, listXmlParams)) {
LOGGER.log(Level.WARNING, Bundle.Cake3Script_redirect_xml_error());
return null;
}
List<Cake3CommandItem> commandsItem = new ArrayList<>();
try {
CakePHP3CommandXmlParser.parse(tmpFile, commandsItem);
} catch (SAXException ex) {
// incorrect xml provided by cakephp?
LOGGER.log(Level.INFO, null, ex);
}
if (commandsItem.isEmpty()) {
// error
tmpFile.delete();
return null;
}
// parse each command
List<FrameworkCommand> commands = new ArrayList<>();
for (Cake3CommandItem item : commandsItem) {
ArrayList<String> commandParams = new ArrayList<>();
// commandParams.addAll(appParam);
commandParams.addAll(Arrays.asList(item.getCommand(), HELP_PARAM, "xml")); // NOI18N
if (!redirectToFile(phpModule, tmpFile, commandParams)) {
commands.add(new Cake3Command(phpModule,
item.getCommand(), item.getDescription(), item.getDisplayName()));
continue;
}
List<Cake3CommandItem> mainCommandsItem = new ArrayList<>();
try {
CakePHP3CommandXmlParser.parse(tmpFile, mainCommandsItem);
} catch (SAXException ex) {
LOGGER.log(Level.WARNING, "Xml file Error:{0}", ex.getMessage());
commands.add(new Cake3Command(phpModule,
item.getCommand(), item.getDescription(), item.getDisplayName()));
continue;
}
if (mainCommandsItem.isEmpty()) {
tmpFile.delete();
return null;
}
// add main command
Cake3CommandItem main = mainCommandsItem.get(0);
String mainCommand = main.getCommand();
String provider = item.getDescription();
commands.add(new Cake3Command(phpModule,
mainCommand, "[" + provider + "] " + main.getDescription(), main.getDisplayName())); // NOI18N
// add subcommands
List<Cake3CommandItem> subcommands = main.getSubcommands();
for (Cake3CommandItem subcommand : subcommands) {
String[] command = {mainCommand, subcommand.getCommand()};
commands.add(new Cake3Command(phpModule,
command, "[" + provider + "] " + subcommand.getDescription(), main.getCommand() + " " + subcommand.getDisplayName())); // NOI18N
}
}
tmpFile.delete();
return commands;
}
@NbBundle.Messages({
"# {0} - exitValue",
"Cake3Script.redirect.error=exitValue:{0} There may be some errors when redirect command result to file"
})
private boolean redirectToFile(PhpModule phpModule, File file, List<String> commands) {
Future<Integer> result = createPhpExecutable(phpModule)
.fileOutput(file, "UTF-8", true) // NOI18N
.warnUser(false)
.additionalParameters(commands)
.run(getSilentDescriptor());
try {
if (result == null) {
// error
return false;
}
// CakePHP 3.x uses exit() in cake script, so, return value is not 0
Integer exitValue = result.get();
if (exitValue != 0) {
if (exitValue != 1) {
LOGGER.log(Level.WARNING, Bundle.Cake3Script_redirect_error(exitValue));
return false;
}
}
} catch (CancellationException | ExecutionException ex) {
return false;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return false;
}
return true;
}
private ExecutionDescriptor getSilentDescriptor() {
return new ExecutionDescriptor()
.inputOutput(InputOutput.NULL);
}
private PhpExecutable createPhpExecutable(PhpModule phpModule) {
CakePHP3Module module = CakePHP3Module.forPhpModule(phpModule);
List<FileObject> directories = module.getDirectories(Base.APP);
PhpExecutable phpExecutable = new PhpExecutable(cakePath)
.viaAutodetection(true);
if (!directories.isEmpty()) {
File workDir = FileUtil.toFile(directories.get(0));
phpExecutable = phpExecutable.workDir(workDir);
}
return phpExecutable;
}
private List<String> getAllParams(List<String> params) {
List<String> allParams = new ArrayList<>();
allParams.addAll(DEFAULT_PARAMS);
allParams.addAll(params);
return allParams;
}
@NbBundle.Messages({
"# {0} - project name",
"# {1} - command",
"Cake3Script.command.title={0} ({1})"
})
private String getDisplayName(PhpModule phpModule, String command) {
return Bundle.Cake3Script_command_title(phpModule.getDisplayName(), command);
}
private ExecutionDescriptor getDescriptor(Runnable postExecution) {
ExecutionDescriptor executionDescriptor = PhpExecutable.DEFAULT_EXECUTION_DESCRIPTOR
.optionsPath(getOptionsPath())
.inputVisible(true);
if (postExecution != null) {
executionDescriptor = executionDescriptor.postExecution(postExecution);
}
return executionDescriptor;
}
private ExecutionDescriptor.InputProcessorFactory2 getOutProcessorFactory(final LineProcessor lineProcessor) {
return new ExecutionDescriptor.InputProcessorFactory2() {
@Override
public InputProcessor newInputProcessor(InputProcessor defaultProcessor) {
if (lineProcessor == null) {
return defaultProcessor;
}
return InputProcessors.ansiStripping(InputProcessors.bridge(lineProcessor));
}
};
}
private static String getOptionsPath() {
return UiUtils.FRAMEWORKS_AND_TOOLS_OPTIONS_PATH;
}
private static class ServerLineProcessor implements LineProcessor {
private static final Pattern LOCALHOST_PATTERN = Pattern.compile("\\A.*(?<localhost>http://localhost:\\d{4}/).*\\z"); // NOI18N
@Override
public void processLine(String line) {
Matcher matcher = LOCALHOST_PATTERN.matcher(line);
if (matcher.find()) {
String localhostUrl = matcher.group("localhost"); // NOI18N
try {
URL url = new URL(localhostUrl);
// TODO add options?
HtmlBrowser.URLDisplayer.getDefault().showURL(url);
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
}
}
}
@Override
public void reset() {
}
@Override
public void close() {
}
}
private static class HelpLineProcessor implements LineProcessor {
private final StringBuilder sb = new StringBuilder();
@Override
public void processLine(String line) {
sb.append(line);
sb.append("\n"); // NOI18N
}
@Override
public void reset() {
}
@Override
public void close() {
}
public String getHelp() {
return sb.toString();
}
}
}