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}