001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.boot; 019 020import io.stallion.exceptions.CommandException; 021import io.stallion.exceptions.UsageException; 022import io.stallion.services.Log; 023import io.stallion.templating.JinjaTemplating; 024import io.stallion.users.UserAdder; 025import io.stallion.utils.Prompter; 026import io.stallion.utils.GeneralUtils; 027import org.apache.commons.io.FileUtils; 028import org.apache.commons.io.IOUtils; 029 030import java.io.File; 031import java.io.IOException; 032import java.nio.charset.Charset; 033import java.nio.file.Paths; 034import java.util.Scanner; 035import java.util.logging.Level; 036 037import static io.stallion.utils.Literals.*; 038 039 040public class NewProjectBuilder implements StallionRunAction<CommandOptionsBase> { 041 Scanner scanner = new Scanner (System.in); 042 private String targetFolder = ""; 043 private CommandOptionsBase options; 044 private JinjaTemplating templating; 045 private ProjectSettingsBuilder builder; 046 047 private boolean shouldMakePages = false; 048 private boolean shouldMakeTemplates = false; 049 private boolean shouldMakeAssets = false; 050 private boolean shouldMakeBlog = false; 051 052 @Override 053 public String getActionName() { 054 return "new"; 055 } 056 057 @Override 058 public String getHelp() { 059 return "Create a new Stallion website"; 060 } 061 062 @Override 063 public void loadApp(CommandOptionsBase options) { 064 065 } 066 067 public boolean requireDatabase() { 068 return false; 069 } 070 071 @Override 072 public CommandOptionsBase newCommandOptions() { 073 return new CommandOptionsBase(); 074 } 075 076 protected void init(CommandOptionsBase options) { 077 String folder = or(options.getTargetPath(), Paths.get(".").toAbsolutePath().normalize().toString()); 078 targetFolder = folder; 079 this.options = options; 080 templating = new JinjaTemplating(targetFolder, false); 081 File dir = new File(folder); 082 if (!dir.getParentFile().isDirectory()) { 083 throw new CommandException("The parent folder does not exist: " + dir.getParentFile().getAbsolutePath()); 084 } 085 File confDir = new File(folder + "/conf"); 086 if (confDir.exists()) { 087 throw new CommandException("You have already initialized the folder " + folder); 088 } 089 confDir = new File(folder + "/site/conf"); 090 if (confDir.exists()) { 091 throw new CommandException("You have already initialized the folder " + folder); 092 } 093 094 } 095 096 @Override 097 public void execute(CommandOptionsBase options) throws Exception { 098 init(options); 099 String msg = "Choose the starting scaffolding: \n" + 100 "1) Barebones\n" + 101 "2) Text File Web Site\n" + 102 "3) Javascript Application\n" + 103 "4) Java Application" + 104 "\n\nChoose a number: "; 105 String choice = new Prompter(msg).setChoices("1", "2", "3", "4").prompt(); 106 if ("1".equals(choice)) { 107 makeBareBonesSite(getTargetFolder()); 108 } else if ("2".equals(choice)) { 109 makeSimpleTextSite(getTargetFolder()); 110 } else if ("3".equals(choice)) { 111 makeJavascriptSite(getTargetFolder()); 112 } else if ("4".equals(choice)) { 113 makeJavaApplication(getTargetFolder()); 114 } else { 115 throw new UsageException("Invalid choice " + choice); 116 } 117 118 119 } 120 121 protected void makeBasicScaffold(String folder) throws Exception { 122 makeStandardConf(); 123 copyFile("/templates/wizard/deployment.toml", "conf/deployment.toml"); 124 copyFile("/templates/wizard/hosts.toml", "conf/hosts.toml"); 125 126 if (shouldMakeTemplates) { 127 makeTemplates(); 128 } 129 if (shouldMakeAssets) { 130 makeAssets(); 131 } 132 if (shouldMakePages) { 133 makePages(); 134 } 135 136 if (shouldMakeBlog) { 137 copyFile("/templates/wizard/post.jinja", "templates/post.jinja"); 138 copyFile("/templates/wizard/listing.jinja", "templates/listing.jinja"); 139 makePosts(); 140 } 141 142 143 boolean shouldMakeUser = new Prompter("Do you want to create an admin user right now? This is needed to use some of the internal tools from the web. You can do this later by running >stallion user-add. (Y/n)? ").yesNo(); 144 if (shouldMakeUser) { 145 makeUser(); 146 } 147 148 149 } 150 151 protected void makeBareBonesSite(String folder) throws Exception { 152 shouldMakeTemplates = true; 153 shouldMakePages = true; 154 shouldMakeAssets = true; 155 makeBasicScaffold(folder); 156 System.out.printf("\n\nYour site is now complete! You can test it out by running >bin/stallion serve\n\n"); 157 } 158 159 protected void makeSimpleTextSite(String folder) throws Exception { 160 shouldMakeBlog = true; 161 shouldMakeTemplates = true; 162 shouldMakePages = true; 163 shouldMakeAssets = true; 164 makeBasicScaffold(folder); 165 System.out.printf("\n\nYour site is now complete! You can test it out by running >bin/stallion serve\n\n"); 166 167 } 168 169 protected void makeJavascriptSite(String folder) throws Exception { 170 shouldMakeTemplates = true; 171 shouldMakePages = true; 172 shouldMakeAssets = true; 173 makeBasicScaffold(folder); 174 removeFile("pages/home.txt"); 175 makeJs(); 176 copyFile("/templates/wizard/app.jinja", "templates/app.jinja"); 177 System.out.printf("\n\nYour site is now complete! You can test it out by running >bin/stallion serve\n\n"); 178 } 179 180 protected void makeJavaApplication(String folder) throws Exception { 181 targetFolder = targetFolder + "/site"; 182 templating = new JinjaTemplating(targetFolder, false); 183 184 File file = new File(targetFolder); 185 if (!file.isDirectory()) { 186 new File(targetFolder).mkdirs(); 187 } 188 if (new File(targetFolder + "/site/conf").exists()) { 189 throw new UsageException("You have already initialized an application at this location."); 190 } 191 192 // Make the simple site 193 shouldMakeTemplates = false; 194 shouldMakePages = false; 195 shouldMakeAssets = false; 196 makeBasicScaffold(folder); 197 removeFile("pages/home.txt"); 198 199 NewJavaPluginRunAction javaAction = new NewJavaPluginRunAction(); 200 file = new File(folder + "/java-app"); 201 if (!file.exists()) { 202 file.mkdirs(); 203 } 204 javaAction.makeNewApp(folder + "/java-app"); 205 System.out.printf("\n\nYour site is now complete! Run it by going to " + folder + "/java-app and running ./run-dev.sh"); 206 } 207 208 protected void makeStandardConf() throws IOException { 209 File file = new File(targetFolder + "/conf"); 210 if (!file.isDirectory()) { 211 new File(targetFolder).mkdirs(); 212 } 213 builder = new ProjectSettingsBuilder() 214 .setMakeBlog(shouldMakeBlog) 215 .setHealthCheckSecret(GeneralUtils.randomToken(20)) 216 .setSiteName(new Prompter("What is the name of your new site? ").prompt()) 217 .setSiteDescription(new Prompter("What is a one or two sentence description of your site? ").prompt()) 218 .setSiteUrl(new Prompter("What is the URL that this site will be publicly available at? ").prompt()) 219 .setTitle(Prompter.prompt("What is the title for your site? ")); 220 builder.setHighlightColor(or(new Prompter("Choose a (hex) highlight color for default pages and emails. Leave empty to default to blue (#2184A5) ") 221 .setValidPattern("(#[a-fA-F0-9]{6,6}|)") 222 .setAllowEmpty(true).prompt(), "#2184A5")); 223 boolean configureEmails = new Prompter("Do you want to configure email sending? Configuring email will allow " + 224 "your Stallion application to email you exceptions, form submissions, comments, et cetera. " + 225 "You will need the username, password, and host for an SMTP server. You can get" + 226 "a free email sending account from a service like Sendgrid, Postmark, or Mailgun. Configure email sending? (Y/n) ").yesNo(); 227 if (configureEmails) { 228 builder.setEmailHost(Prompter.prompt("Email host? ")); 229 builder.setEmailPort(Long.parseLong(or(new Prompter("Email port? (Default is 587) ").setAllowEmpty(true).prompt(), "587"))); 230 builder.setAdminEmail(new Prompter("Administrator email address (will get exception messages, etc.)? ").setValidPattern(".+@.+").prompt()); 231 builder.setEmailPassword(Prompter.prompt("Email password? ")); 232 builder.setEmailUsername(Prompter.prompt("Email username? ")); 233 } 234 boolean configureDatabase = requireDatabase(); 235 if (!configureDatabase) { 236 configureDatabase = new Prompter("Configure a local database? (Y/n) ").yesNo(); 237 } 238 if (configureDatabase) { 239 builder.setDatabaseUrl(Prompter.prompt("Database URL?" )); 240 builder.setDatabaseUsername(Prompter.prompt("Database username? ")); 241 builder.setDatabasePassword(new Prompter("Database password?" ).setAllowEmpty(true).prompt()); 242 } 243 244 245 246 247 String source = templating.renderTemplate( 248 IOUtils.toString(getClass().getResource("/templates/wizard/stallion.toml.jinja"), UTF8), 249 map(val("builder", builder))); 250 String prodSource = templating.renderTemplate( 251 IOUtils.toString(getClass().getResource("/templates/wizard/stallion.prod.toml.jinja"), UTF8), 252 map(val("builder", builder))); 253 FileUtils.writeStringToFile(new File(targetFolder + "/conf/stallion.toml"), source, "UTF-8"); 254 FileUtils.writeStringToFile(new File(targetFolder + "/conf/stallion.prod.toml"), prodSource, "UTF-8"); 255 } 256 257 protected void makePages() throws IOException { 258 File f = new File(targetFolder + "/pages"); 259 if (!f.isDirectory()) { 260 f.mkdirs(); 261 } 262 copyFile("/templates/wizard/home.txt", "pages/home.txt"); 263 copyFile("/templates/wizard/about.txt", "pages/about.txt"); 264 copyFile("/templates/wizard/contact-us.txt", "pages/contact-us.txt"); 265 } 266 267 protected void makePosts() throws IOException { 268 File f = new File(targetFolder + "/posts"); 269 if (!f.isDirectory()) { 270 f.mkdirs(); 271 } 272 copyFile("/templates/wizard/first-post.txt", "posts/first-post.txt"); 273 copyFile("/templates/wizard/second-post.txt", "posts/second-post.txt"); 274 } 275 276 protected void makeTemplates() throws IOException { 277 File f = new File(targetFolder + "/templates"); 278 if (!f.isDirectory()) { 279 f.mkdirs(); 280 } 281 copyFile("/templates/wizard/page.jinja", "templates/page.jinja"); 282 copyFile("/templates/wizard/contact-us.jinja", "templates/contact-us.jinja"); 283 //copyFile("/templates/wizard/home.jinja", "templates/home.jinja"); 284 copyFile("/templates/wizard/base.jinja", "templates/base.jinja"); 285 286 } 287 288 protected void removeFile(String relativePath) throws IOException { 289 File file = new File(targetFolder + "/" + relativePath); 290 if (file.isFile()) { 291 file.delete(); 292 } 293 } 294 295 protected void makeAssets() throws IOException { 296 File f = new File(targetFolder + "/assets"); 297 if (!f.isDirectory()) { 298 f.mkdirs(); 299 } 300 301 copyFile("/templates/wizard/site.css", "assets/site.css"); 302 String colorsCss = "a {\n" + 303 " color: " + builder.getHighlightColor() + ";\n" + 304 "}\n" + 305 ".sidebar, .pure-button-primary {\n" + 306 " background: " + builder.getHighlightColor() + ";\n" + 307 "}"; 308 replaceString("/assets/site.css", "/**--highlight-color-section--*/", colorsCss); 309 copyFile("/templates/wizard/site.js", "assets/site.js"); 310 copyFile("/templates/wizard/site.bundle", "assets/site.bundle"); 311 312 313 } 314 315 protected void makeUser() throws Exception { 316 Log.setLogLevel(Level.WARNING); 317 AppContextLoader.loadCompletely(options); 318 UserAdder adder = new UserAdder(); 319 adder.execute(options, "new"); 320 } 321 322 public void makeJs() throws Exception { 323 File file = new File(targetFolder + "/js"); 324 file.mkdirs(); 325 copyFile("/templates/wizard/main.js", "js/main.js"); 326 } 327 328 public void copyFile(String resourcePath, String relativePath) throws IOException { 329 File file = new File(targetFolder + "/" + relativePath); 330 String source = IOUtils.toString(getClass().getResource(resourcePath), UTF8); 331 FileUtils.writeStringToFile(file, source, "utf-8"); 332 } 333 334 335 protected void replaceString(String relativePath, String old, String newString) { 336 File file = new File(getTargetFolder() + "/" + relativePath); 337 338 try { 339 String content = FileUtils.readFileToString(file, UTF8); 340 content = content.replace(old, newString); 341 FileUtils.write(file, content, Charset.forName("utf-8")); 342 } catch (IOException e) { 343 throw new RuntimeException(e); 344 } 345 346 } 347 348 349 public Scanner getScanner() { 350 return scanner; 351 } 352 353 public NewProjectBuilder setScanner(Scanner scanner) { 354 this.scanner = scanner; 355 return this; 356 } 357 358 protected String getTargetFolder() { 359 return targetFolder; 360 } 361 362 protected NewProjectBuilder setTargetFolder(String targetFolder) { 363 this.targetFolder = targetFolder; 364 return this; 365 } 366 367 protected CommandOptionsBase getOptions() { 368 return options; 369 } 370 371 protected NewProjectBuilder setOptions(CommandOptionsBase options) { 372 this.options = options; 373 return this; 374 } 375 376 protected JinjaTemplating getTemplating() { 377 return templating; 378 } 379 380 protected NewProjectBuilder setTemplating(JinjaTemplating templating) { 381 this.templating = templating; 382 return this; 383 } 384}