99aea55e58a04ec774dfae286175f8cc5395393b
[idea/community.git] / platform / lang-impl / src / com / intellij / platform / templates / SaveProjectAsTemplateAction.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.platform.templates;
17
18 import com.intellij.CommonBundle;
19 import com.intellij.ide.fileTemplates.FileTemplate;
20 import com.intellij.ide.fileTemplates.FileTemplateManager;
21 import com.intellij.ide.fileTemplates.FileTemplateUtil;
22 import com.intellij.ide.impl.ProjectUtil;
23 import com.intellij.ide.util.projectWizard.ProjectTemplateFileProcessor;
24 import com.intellij.ide.util.projectWizard.ProjectTemplateParameterFactory;
25 import com.intellij.openapi.actionSystem.AnAction;
26 import com.intellij.openapi.actionSystem.AnActionEvent;
27 import com.intellij.openapi.application.ApplicationManager;
28 import com.intellij.openapi.application.WriteAction;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.extensions.Extensions;
31 import com.intellij.openapi.module.Module;
32 import com.intellij.openapi.progress.PerformInBackgroundOption;
33 import com.intellij.openapi.progress.ProgressIndicator;
34 import com.intellij.openapi.progress.ProgressManager;
35 import com.intellij.openapi.progress.Task;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.roots.ContentIterator;
38 import com.intellij.openapi.roots.FileIndex;
39 import com.intellij.openapi.roots.ModuleRootManager;
40 import com.intellij.openapi.roots.ProjectRootManager;
41 import com.intellij.openapi.ui.Messages;
42 import com.intellij.openapi.util.JDOMUtil;
43 import com.intellij.openapi.util.io.FileUtil;
44 import com.intellij.openapi.util.io.StreamUtil;
45 import com.intellij.openapi.vfs.CharsetToolkit;
46 import com.intellij.openapi.vfs.VfsUtil;
47 import com.intellij.openapi.vfs.VfsUtilCore;
48 import com.intellij.openapi.vfs.VirtualFile;
49 import com.intellij.util.io.ZipUtil;
50 import com.intellij.util.ui.UIUtil;
51 import gnu.trove.TIntObjectHashMap;
52 import org.jdom.Element;
53 import org.jetbrains.annotations.NotNull;
54 import org.jetbrains.annotations.Nullable;
55
56 import java.io.*;
57 import java.util.HashMap;
58 import java.util.Map;
59 import java.util.regex.Matcher;
60 import java.util.regex.Pattern;
61 import java.util.zip.ZipEntry;
62 import java.util.zip.ZipOutputStream;
63
64 /**
65  * @author Dmitry Avdeev
66  *         Date: 10/5/12
67  */
68 public class SaveProjectAsTemplateAction extends AnAction {
69
70   private static final Logger LOG = Logger.getInstance(SaveProjectAsTemplateAction.class);
71   private static final String PROJECT_TEMPLATE_XML = "project-template.xml";
72
73   @Override
74   public void actionPerformed(AnActionEvent e) {
75     final Project project = getEventProject(e);
76     assert project != null;
77     if (!ProjectUtil.isDirectoryBased(project)) {
78       Messages.showErrorDialog(project, "Project templates do not support old .ipr (file-based) format.\n" +
79                                         "Please convert your project via File->Save as Directory-Based format.", CommonBundle.getErrorTitle());
80       return;
81     }
82
83     final VirtualFile descriptionFile = getDescriptionFile(project, LocalArchivedTemplate.DESCRIPTION_PATH);
84     final SaveProjectAsTemplateDialog dialog = new SaveProjectAsTemplateDialog(project, descriptionFile);
85
86     if (dialog.showAndGet()) {
87
88       final Module moduleToSave = dialog.getModuleToSave();
89       final File file = dialog.getTemplateFile();
90       final String description = dialog.getDescription();
91
92       ProgressManager.getInstance().run(new Task.Backgroundable(project, "Saving Project as Template", true, PerformInBackgroundOption.DEAF) {
93         @Override
94         public void run(@NotNull final ProgressIndicator indicator) {
95           saveProject(project, file, moduleToSave, description, dialog.isReplaceParameters(), indicator);
96         }
97
98         @Override
99         public void onSuccess() {
100           Messages.showInfoMessage(FileUtil.getNameWithoutExtension(file) + " was successfully created.\n" +
101                                    "It's available now in Project Wizard", "Template Created");
102         }
103
104         @Override
105         public void onCancel() {
106           file.delete();
107         }
108       });
109     }
110   }
111
112   public static VirtualFile getDescriptionFile(Project project, String path) {
113     VirtualFile baseDir = project.getBaseDir();
114     return baseDir != null ? baseDir.findFileByRelativePath(path) : null;
115   }
116
117   public static void saveProject(final Project project,
118                                  final File zipFile,
119                                  Module moduleToSave,
120                                  final String description,
121                                  boolean replaceParameters,
122                                  final ProgressIndicator indicator) {
123
124     final Map<String, String> parameters = computeParameters(project, replaceParameters);
125     indicator.setText("Saving project...");
126     ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(project::save),
127                                                       indicator.getModalityState());
128     indicator.setText("Processing project files...");
129     ZipOutputStream stream = null;
130     try {
131       FileUtil.ensureExists(zipFile.getParentFile());
132       stream = new ZipOutputStream(new FileOutputStream(zipFile));
133
134       final VirtualFile dir = getDirectoryToSave(project, moduleToSave);
135       writeFile(LocalArchivedTemplate.DESCRIPTION_PATH, description, project, dir, stream, true, indicator);
136       if (replaceParameters) {
137         String text = getInputFieldsText(parameters);
138         writeFile(LocalArchivedTemplate.TEMPLATE_DESCRIPTOR, text, project, dir, stream, false, indicator);
139       }
140
141       FileIndex index = moduleToSave == null
142                         ? ProjectRootManager.getInstance(project).getFileIndex()
143                         : ModuleRootManager.getInstance(moduleToSave).getFileIndex();
144       final ZipOutputStream finalStream = stream;
145
146       index.iterateContent(new ContentIterator() {
147         @Override
148         public boolean processFile(final VirtualFile virtualFile) {
149           if (!virtualFile.isDirectory()) {
150             final String fileName = virtualFile.getName();
151             indicator.setText2(fileName);
152             try {
153               String relativePath = VfsUtilCore.getRelativePath(virtualFile, dir, '/');
154               if (relativePath == null) {
155                 throw new RuntimeException("Can't find relative path for " + virtualFile + " in " + dir);
156               }
157               final boolean system = Project.DIRECTORY_STORE_FOLDER.equals(virtualFile.getParent().getName());
158               if (system) {
159                 if (!fileName.equals("description.html") &&
160                     !fileName.equals(PROJECT_TEMPLATE_XML) &&
161                     !fileName.equals("misc.xml") &&
162                     !fileName.equals("modules.xml") &&
163                     !fileName.equals("workspace.xml") &&
164                     !fileName.endsWith(".iml")) {
165                   return true;
166                 }
167               }
168
169               ZipUtil.addFileToZip(finalStream, new File(virtualFile.getPath()), dir.getName() + "/" + relativePath, null, null, new ZipUtil.FileContentProcessor() {
170                 @Override
171                 public InputStream getContent(final File file) throws IOException {
172                   if (virtualFile.getFileType().isBinary() || PROJECT_TEMPLATE_XML.equals(virtualFile.getName())) return STANDARD.getContent(file);
173                   String result = getEncodedContent(virtualFile, project, parameters);
174                   return new ByteArrayInputStream(result.getBytes(CharsetToolkit.UTF8_CHARSET));
175                 }
176               });
177             }
178             catch (IOException e) {
179               LOG.error(e);
180             }
181           }
182           indicator.checkCanceled();
183           return true;
184         }
185       });
186     }
187     catch (Exception ex) {
188       LOG.error(ex);
189       UIUtil.invokeLaterIfNeeded(new Runnable() {
190         public void run() {
191           Messages.showErrorDialog(project, "Can't save project as template", "Internal Error");
192         }
193       });
194     }
195     finally {
196       StreamUtil.closeStream(stream);
197     }
198   }
199
200   private static void writeFile(String path,
201                                 final String text,
202                                 Project project, VirtualFile dir, ZipOutputStream stream, boolean overwrite, ProgressIndicator indicator) throws IOException {
203     final VirtualFile descriptionFile = getDescriptionFile(project, path);
204     if (descriptionFile == null) {
205       stream.putNextEntry(new ZipEntry(dir.getName() + "/" + path));
206       stream.write(text.getBytes());
207       stream.closeEntry();
208     }
209     else if (overwrite) {
210       ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(() -> {
211         try {
212           VfsUtil.saveText(descriptionFile, text);
213         }
214         catch (IOException e) {
215           LOG.error(e);
216         }
217       }), indicator.getModalityState());
218     }
219   }
220
221   public static Map<String, String> computeParameters(final Project project, boolean replaceParameters) {
222     final Map<String, String> parameters = new HashMap<String, String>();
223     if (replaceParameters) {
224       ApplicationManager.getApplication().runReadAction(new Runnable() {
225         public void run() {
226           ProjectTemplateParameterFactory[] extensions = Extensions.getExtensions(ProjectTemplateParameterFactory.EP_NAME);
227           for (ProjectTemplateParameterFactory extension : extensions) {
228             String value = extension.detectParameterValue(project);
229             if (value != null) {
230               parameters.put(value, extension.getParameterId());
231             }
232           }
233         }
234       });
235     }
236     return parameters;
237   }
238
239   public static String getEncodedContent(VirtualFile virtualFile,
240                                           Project project,
241                                           Map<String, String> parameters) throws IOException {
242     String text = VfsUtilCore.loadText(virtualFile);
243     final FileTemplate template = FileTemplateManager.getInstance(project).getDefaultTemplate(FileTemplateManager.FILE_HEADER_TEMPLATE_NAME);
244     final String templateText = template.getText();
245     final Pattern pattern = FileTemplateUtil.getTemplatePattern(template, project, new TIntObjectHashMap<String>());
246     String result = convertTemplates(text, pattern, templateText);
247     result = ProjectTemplateFileProcessor.encodeFile(result, virtualFile, project);
248     for (Map.Entry<String, String> entry : parameters.entrySet()) {
249       result = result.replace(entry.getKey(), "${" + entry.getValue() + "}");
250     }
251     return result;
252   }
253
254   private static VirtualFile getDirectoryToSave(Project project, @Nullable Module module) {
255     if (module == null) {
256       return project.getBaseDir();
257     }
258     else {
259       VirtualFile moduleFile = module.getModuleFile();
260       assert moduleFile != null;
261       return moduleFile.getParent();
262     }
263   }
264
265   public static String convertTemplates(String input, Pattern pattern, String template) {
266     Matcher matcher = pattern.matcher(input);
267     int start = matcher.matches() ? matcher.start(1) : -1;
268     StringBuilder builder = new StringBuilder(input.length() + 10);
269     for (int i = 0; i < input.length(); i++) {
270       if (start == i) {
271         builder.append(template);
272         //noinspection AssignmentToForLoopParameter
273         i = matcher.end(1);
274       }
275
276       char c = input.charAt(i);
277       if (c == '$' || c == '#') {
278         builder.append('\\');
279       }
280       builder.append(c);
281     }
282     return builder.toString();
283   }
284
285   private static String getInputFieldsText(Map<String, String> parameters) {
286     Element element = new Element(ArchivedProjectTemplate.TEMPLATE);
287     for (Map.Entry<String, String> entry : parameters.entrySet()) {
288       Element field = new Element(ArchivedProjectTemplate.INPUT_FIELD);
289       field.setText(entry.getValue());
290       field.setAttribute(ArchivedProjectTemplate.INPUT_DEFAULT, entry.getKey());
291       element.addContent(field);
292     }
293     return JDOMUtil.writeElement(element);
294   }
295
296   @Override
297   public void update(AnActionEvent e) {
298     Project project = getEventProject(e);
299     e.getPresentation().setEnabled(project != null && !project.isDefault());
300   }
301 }