001/*
002  GRANITE DATA SERVICES
003  Copyright (C) 2011 GRANITE DATA SERVICES S.A.S.
004
005  This file is part of Granite Data Services.
006
007  Granite Data Services is free software; you can redistribute it and/or modify
008  it under the terms of the GNU Library General Public License as published by
009  the Free Software Foundation; either version 2 of the License, or (at your
010  option) any later version.
011
012  Granite Data Services is distributed in the hope that it will be useful, but
013  WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
014  FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License
015  for more details.
016
017  You should have received a copy of the GNU Library General Public License
018  along with this library; if not, see <http://www.gnu.org/licenses/>.
019*/
020
021package org.granite.builder;
022
023import java.io.File;
024import java.io.FileFilter;
025import java.text.DateFormat;
026import java.util.ArrayList;
027import java.util.Date;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.regex.Pattern;
033
034import org.eclipse.core.resources.IFile;
035import org.eclipse.core.resources.IProject;
036import org.eclipse.core.resources.IResource;
037import org.eclipse.core.resources.IResourceDelta;
038import org.eclipse.core.resources.IResourceDeltaVisitor;
039import org.eclipse.core.resources.IResourceVisitor;
040import org.eclipse.core.resources.IncrementalProjectBuilder;
041import org.eclipse.core.runtime.CoreException;
042import org.eclipse.core.runtime.IPath;
043import org.eclipse.core.runtime.IProgressMonitor;
044import org.eclipse.core.runtime.NullProgressMonitor;
045import org.granite.builder.properties.Gas3Source;
046import org.granite.builder.properties.Gas3Transformer;
047import org.granite.builder.properties.GranitePropertiesLoader;
048import org.granite.builder.ui.AddNatureWizard;
049import org.granite.builder.util.BuilderUtil;
050import org.granite.builder.util.FileUtil;
051import org.granite.builder.util.FlexConfigGenerator;
052import org.granite.builder.util.JavaClassInfo;
053import org.granite.builder.util.ProjectUtil;
054import org.granite.generator.Generator;
055import org.granite.generator.Listener;
056import org.granite.generator.Output;
057import org.granite.generator.Transformer;
058import org.granite.generator.as3.JavaAs3Input;
059import org.granite.generator.as3.JavaAs3Output;
060import org.granite.generator.as3.PackageTranslator;
061
062/**
063 * @author Franck WOLFF
064 */
065public class GraniteBuilder extends IncrementalProjectBuilder {
066
067    
068    public static final String JAVA_BUILDER_ID = "org.eclipse.jdt.core.javabuilder";
069    public static final String FLEX_BUILDER_ID = "com.adobe.flexbuilder.project.flexbuilder";
070    public static final String GRANITE_BUILDER_ID = "org.granite.builder.granitebuilder";
071
072    private static final int PROGRESS_TOTAL = 100;
073
074    private final Generator generator;
075    private final BuilderListener listener;
076    private BuilderConfiguration config;
077
078    ///////////////////////////////////////////////////////////////////////////
079    // Constructor.
080
081    public GraniteBuilder() {
082                super();
083                this.generator = new Generator();
084                this.listener = new BuilderListener();
085        }
086
087    ///////////////////////////////////////////////////////////////////////////
088    // Build.
089
090        @SuppressWarnings("rawtypes")
091    @Override
092    protected IProject[] build(int kind, Map args, IProgressMonitor monitor) throws CoreException {
093        listener.title("Building project \"" + getProject().getName() + "\" (" + DateFormat.getInstance().format(new Date()) + ")...");
094        long t0 = System.currentTimeMillis();
095        
096        
097        GenerationResult result = null;
098        try {
099                if (!GranitePropertiesLoader.exists(getProject())) {
100                        BuilderConsole.activate();
101                AddNatureWizard.run(getProject());
102                        config = null;
103                        generator.clear();
104                } else if (args.containsKey(GraniteRebuildJob.RESET_KEY) || (config != null && config.isOutdated())) {
105                        config = null;
106                        generator.clear();
107                }
108                
109                config = getConfig();
110                config.resetClassLoader();
111                config.getGroovyTemplateFactory().cleanOutdated();
112                
113                generator.setConfig(config);
114                
115                BuilderConsole.setDebugEnabled(config.getProperties().getGas3().isDebugEnabled());
116                        
117                if (generator.isEmpty()) {
118                        for (Gas3Transformer gas3Transformer : config.getProperties().getGas3().getTransformers()) {
119                        try {
120                                Transformer<?,?,?> transformer = BuilderUtil.newInstance(Transformer.class, gas3Transformer.getType(), config.getClassLoader());
121                                transformer.setListener(listener);
122                                generator.add(transformer);
123                        } catch (Exception e) {
124                                listener.error("Could not load transformer: " + gas3Transformer.getType(), e);
125                                
126                                if (e instanceof CoreException)
127                                        throw (CoreException)e;
128                                if (e.getCause() instanceof CoreException)
129                                        throw (CoreException)e.getCause();
130                                throw new CoreException(ProjectUtil.createErrorStatus(
131                                        "Could not load transformer: " + gas3Transformer.getType(), null
132                                ));
133                        }
134                        }
135                }
136        
137                
138                if (monitor == null)
139                    monitor = new NullProgressMonitor();
140        
141                try {
142                    if (kind == FULL_BUILD)
143                        result = fullBuild(monitor);
144                    else {
145                        IResourceDelta delta = getDelta(getProject());
146                        if (delta == null)
147                                result = fullBuild(monitor);
148                        else
149                                result = incrementalBuild(delta, monitor);
150                    }
151                } catch (CoreException e) {
152                    throw e;
153                } catch (Exception e) {
154                    throw new CoreException(ProjectUtil.createErrorStatus("Granite Build Failed", e));
155                }
156                
157                boolean refreshFlexConfig = false;
158                try {
159                        if (result.generateFlexConfig && config.getProperties().getGas3().isFlexConfig())
160                                refreshFlexConfig = FlexConfigGenerator.generateFlexConfig(config, listener, getProject());
161                }
162                catch (Exception e) {
163                        listener.warn("Could not generate Flex Builder configuration", e);
164                }
165        
166                File projectDir = ProjectUtil.getProjectFile(getProject());
167                for (File dir : result.dirsToRefresh) {
168                        StringBuilder relativePath = new StringBuilder();
169                        while (dir != null && !dir.equals(projectDir)) {
170                                relativePath.insert(0, '/').insert(1, dir.getName());
171                                dir = dir.getParentFile();
172                        }
173                        getProject().getFolder(relativePath.toString()).refreshLocal(IResource.DEPTH_INFINITE, monitor);
174                }
175                if (refreshFlexConfig)
176                        getProject().getFile(FlexConfigGenerator.FILE_NAME).refreshLocal(IResource.DEPTH_ZERO, monitor);
177        }
178        finally {
179                long t1 = System.currentTimeMillis();
180                
181                if (result != null) {
182                        listener.title(
183                                "Done (" + (result.affectedFiles > 0 ? result.affectedFiles + " affected files" : "nothing to do") +
184                                " - " + (t1 - t0) + "ms)."
185                        );
186                } else
187                        listener.title("Done (error) - " + (t1 - t0) + "ms).");
188                
189                listener.title("");
190        }
191        
192        return null;
193    }
194        
195        class GenerationResult {
196                public int affectedFiles = 0;
197                public Set<File> dirsToRefresh = new HashSet<File>();
198                public boolean generateFlexConfig = false;
199        }
200
201    ///////////////////////////////////////////////////////////////////////////
202    // Full Build.
203
204    private GenerationResult fullBuild(final IProgressMonitor monitor) throws CoreException {
205        monitor.beginTask("Granite Full Build", PROGRESS_TOTAL);
206        FullBuildVisitor visitor = new FullBuildVisitor(monitor);
207        try {
208            getProject().accept(visitor);
209        } finally {
210            monitor.done();
211        }
212        return visitor.getResult();
213    }
214
215    class FullBuildVisitor implements IResourceVisitor {
216
217        private IProgressMonitor monitor;
218        private GenerationResult result = new GenerationResult();
219
220        public FullBuildVisitor(IProgressMonitor monitor) {
221            this.monitor = monitor;
222        }
223
224        @Override
225                public boolean visit(IResource resource) throws CoreException {
226            if (!resource.isAccessible() || resource.isPhantom())
227                return false;
228
229            Output<?>[] outputs = generate(resource, monitor);
230            if (outputs != null) {
231                for (Output<?> output : outputs) {
232                        if (output.isOutdated()) {
233                                result.affectedFiles++;
234                                result.dirsToRefresh.add(((JavaAs3Output)output).getDir());
235                        }
236                }
237            }
238                        result.generateFlexConfig = true;
239
240            return true;
241        }
242
243                public GenerationResult getResult() {
244                        return result;
245                }
246    }
247
248    ///////////////////////////////////////////////////////////////////////////
249    // Incremental Build.
250
251    /*
252     * TODO: this part should be refactored, it assumes several things about
253     * generated (.as extension, Base suffix, etc.) which should be deferred
254     * to transformers...
255     */
256    
257    private GenerationResult incrementalBuild(IResourceDelta delta, IProgressMonitor monitor) throws CoreException {
258        monitor.beginTask("Granite Incremental Build", PROGRESS_TOTAL);
259        IncrementalBuildVisitor visitor = new IncrementalBuildVisitor(monitor);
260        try {
261            delta.accept(visitor);
262        } finally {
263            monitor.done();
264        }
265        return visitor.getResult();
266    }
267
268    class IncrementalBuildVisitor implements IResourceDeltaVisitor {
269
270        private IProgressMonitor monitor;
271        private GenerationResult result = new GenerationResult();
272
273        public IncrementalBuildVisitor(IProgressMonitor monitor) {
274            this.monitor = monitor;
275        }
276
277        @Override
278                public boolean visit(IResourceDelta delta) throws CoreException {
279            IResource resource = delta.getResource();
280            String extension = resource.getFileExtension();
281
282            Output<?>[] outputs = null;
283            
284            switch (delta.getKind()) {
285            case IResourceDelta.ADDED:
286                if (!resource.isAccessible() || resource.isPhantom())
287                    return false;
288                outputs = generate(resource, monitor);
289                if (!result.generateFlexConfig)
290                        result.generateFlexConfig = "as".equals(resource.getFileExtension());
291                break;
292            case IResourceDelta.REMOVED:
293                if (!result.generateFlexConfig)
294                        result.generateFlexConfig = "as".equals(extension);
295                outputs = remove(resource, monitor);
296                break;
297            case IResourceDelta.CHANGED:
298                if (!resource.isAccessible() || resource.isPhantom())
299                    return false;
300                outputs = generate(resource, monitor);
301                break;
302            }
303            
304            if (outputs != null) {
305                for (Output<?> output : outputs) {
306                        if (output.isOutdated()) {
307                                result.affectedFiles++;
308                                result.dirsToRefresh.add(((JavaAs3Output)output).getDir());
309                                result.generateFlexConfig = true;
310                        }
311                }
312            }
313
314            return true;
315        }
316
317                public GenerationResult getResult() {
318                        return result;
319                }
320    }
321    
322    private BuilderConfiguration getConfig() {
323        if (config == null || config.isOutdated())
324                config = new BuilderConfiguration(listener, getProject());
325        return config;
326    }
327
328    private Output<?>[] generate(IResource resource, IProgressMonitor monitor) {
329        if (resource instanceof IFile && "class".equals(resource.getFileExtension())) {
330            IFile file = (IFile)resource;
331                        
332            try {
333                    JavaClassInfo info = ProjectUtil.getJavaClassInfo(config.getJavaProject(), (IFile)resource);
334                    if (info == null) {
335                        listener.warn("Could not get class informations for: " + resource.toString());
336                        return null;
337                    }
338                    
339                    Gas3Source source = config.getProperties().getGas3().getMatchingSource(
340                        info.getSourceFolderPath(),
341                        info.getSourceFilePath()
342                    );
343                    
344                    if (source != null) {
345                            monitor.subTask("Generating AS3 code for: " + file.getProjectRelativePath().toString());
346                            try {
347                                Class<?> clazz = config.getClassLoader().loadClass(info.getClassName());
348                                if (!clazz.isAnonymousClass() && config.isGenerated(clazz)) {
349                                        Map<String, String> attributes = source.getAttributes(info.getSourceFolderPath(), info.getSourceFilePath());
350                                        JavaAs3Input input = new BuilderJavaClientInput(clazz, info.getClassFile(), source, attributes);
351                                        return generator.generate(input);
352                                }
353                            } finally {
354                                monitor.worked(1);
355                            }
356                    }
357            } catch (Throwable t) {
358                listener.error("", t);
359            }
360        }
361        
362        return null;
363    }
364    
365    private Output<?>[] remove(IResource resource, IProgressMonitor monitor) {
366        if (resource instanceof IFile && "java".equals(resource.getFileExtension())) {
367            try {
368                IPath resourcePath = resource.getFullPath();
369                IPath resourceSourceFolder = null;
370                
371                    List<IPath> sourceFolders = ProjectUtil.getSourceFolders(config.getJavaProject());
372                    for (IPath sourceFolder : sourceFolders) {
373                        if (sourceFolder.isPrefixOf(resourcePath)) {
374                                resourceSourceFolder = sourceFolder;
375                                break;
376                        }
377                    }
378                    
379                    if (resourceSourceFolder == null)
380                        return null;
381            
382                    String relativeJavaFile = FileUtil.makeRelativeTo(resourceSourceFolder, resourcePath).toPortableString();
383                    Gas3Source source = config.getProperties().getGas3().getMatchingSource(
384                        FileUtil.makeRelativeTo(config.getJavaProject().getPath(), resourceSourceFolder).toPortableString(),
385                        relativeJavaFile
386                    );
387                    
388                    if (source != null) {
389                        
390                        String packageName = "";
391                        String className = relativeJavaFile.substring(0, relativeJavaFile.length() - 5);
392                        
393                        int lastSlash = className.lastIndexOf('/');
394                        if (lastSlash != -1) {
395                                packageName = className.substring(0, lastSlash).replace('/', '.');
396                                PackageTranslator translator =  config.getPackageTranslator(packageName);
397                                if (translator != null)
398                                        packageName = translator.translate(packageName);
399                                className = className.substring(lastSlash + 1);
400                        }
401                        
402                        
403                            monitor.subTask("Removing AS3 code for: " + resource.getProjectRelativePath().toString());
404                            try {
405                                JavaAs3Input input = new BuilderJavaClientInput(null, null, source, null);
406                                File outputDir = config.getBaseOutputDir(input);
407                                
408                                String outputPrefix = outputDir.getName() + File.separator + packageName.replace('.', File.separatorChar) + File.separator + className;
409                                final Pattern pattern = Pattern.compile("^.*" + Pattern.quote(outputPrefix) + "(\\$.*)?(Base)?\\.as$");
410                                List<File> matches = listFiles(outputDir, new FileFilter() {
411                                                        @Override
412                                                        public boolean accept(File file) {
413                                                                return pattern.matcher(file.getPath()).matches();
414                                                        }
415                                });
416                                
417                                if (!matches.isEmpty()) {
418                                        Output<?>[] outputs = new Output<?>[matches.size()];
419                                        for (int i = 0; i < matches.size(); i++) {
420                                                File file = matches.get(i);
421                                                File renameToFile = new File(file.getParentFile(), file.getName() + "." + System.currentTimeMillis() + ".hid");
422                                                outputs[i] = new JavaAs3Output(null, null, renameToFile.getParentFile(), renameToFile, true, Listener.MSG_FILE_REMOVED);
423                                                listener.removing(input, outputs[i]);
424                                                file.renameTo(renameToFile);
425                                        }
426                                        
427                                        return outputs;
428                                }
429                            } finally {
430                                monitor.worked(1);
431                            }
432                    }
433                } catch (Throwable t) {
434                listener.error("", t);
435            }
436        }
437        
438        return null;
439    }
440    
441    private List<File> listFiles(File root, FileFilter filter) {
442        final List<File> files = new ArrayList<File>();
443        listFiles(files, root, filter);
444        return files;
445    }
446
447    private void listFiles(final List<File> files, final File parent, final FileFilter filter) {
448        parent.listFiles(new FileFilter() {
449                        @Override
450                        public boolean accept(File file) {
451                                if (file.isDirectory())
452                                        listFiles(files, file, filter);
453                                if (filter.accept(file))
454                                        files.add(file);
455                                return false;
456                        }
457        });
458    }
459}