1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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
17package com.android.ide.eclipse.adt.internal.build.builders;
18
19import com.android.annotations.NonNull;
20import com.android.annotations.Nullable;
21import com.android.ide.common.sdk.LoadStatus;
22import com.android.ide.eclipse.adt.AdtConstants;
23import com.android.ide.eclipse.adt.AdtPlugin;
24import com.android.ide.eclipse.adt.internal.build.BuildHelper;
25import com.android.ide.eclipse.adt.internal.build.Messages;
26import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
27import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
28import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
29import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler;
30import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener;
31import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
32import com.android.ide.eclipse.adt.internal.sdk.Sdk;
33import com.android.ide.eclipse.adt.io.IFileWrapper;
34import com.android.io.IAbstractFile;
35import com.android.io.StreamException;
36import com.android.sdklib.BuildToolInfo;
37import com.android.sdklib.IAndroidTarget;
38import com.android.sdklib.repository.FullRevision;
39
40import org.eclipse.core.resources.IContainer;
41import org.eclipse.core.resources.IFile;
42import org.eclipse.core.resources.IFolder;
43import org.eclipse.core.resources.IMarker;
44import org.eclipse.core.resources.IProject;
45import org.eclipse.core.resources.IResource;
46import org.eclipse.core.resources.IncrementalProjectBuilder;
47import org.eclipse.core.resources.ResourcesPlugin;
48import org.eclipse.core.runtime.CoreException;
49import org.eclipse.core.runtime.IPath;
50import org.eclipse.core.runtime.IProgressMonitor;
51import org.eclipse.core.runtime.jobs.Job;
52import org.eclipse.jdt.core.IJavaProject;
53import org.xml.sax.SAXException;
54
55import java.util.ArrayList;
56
57import javax.xml.parsers.ParserConfigurationException;
58import javax.xml.parsers.SAXParser;
59import javax.xml.parsers.SAXParserFactory;
60
61/**
62 * Base builder for XML files. This class allows for basic XML parsing with
63 * error checking and marking the files for errors/warnings.
64 */
65public abstract class BaseBuilder extends IncrementalProjectBuilder {
66
67    protected static final boolean DEBUG_LOG = "1".equals(              //$NON-NLS-1$
68            System.getenv("ANDROID_BUILD_DEBUG"));                      //$NON-NLS-1$
69
70    /** SAX Parser factory. */
71    private SAXParserFactory mParserFactory;
72
73    /**
74     * The build tool to use to build. This is guaranteed to be non null after a call to
75     * {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be
76     * queried.
77     */
78    protected BuildToolInfo mBuildToolInfo;
79
80    /**
81     * Base Resource Delta Visitor to handle XML error
82     */
83    protected static class BaseDeltaVisitor implements XmlErrorListener {
84
85        /** The Xml builder used to validate XML correctness. */
86        protected BaseBuilder mBuilder;
87
88        /**
89         * XML error flag. if true, we keep parsing the ResourceDelta but the
90         * compilation will not happen (we're putting markers)
91         */
92        public boolean mXmlError = false;
93
94        public BaseDeltaVisitor(BaseBuilder builder) {
95            mBuilder = builder;
96        }
97
98        /**
99         * Finds a matching Source folder for the current path. This checks if the current path
100         * leads to, or is a source folder.
101         * @param sourceFolders The list of source folders
102         * @param pathSegments The segments of the current path
103         * @return The segments of the source folder, or null if no match was found
104         */
105        protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders,
106                String[] pathSegments) {
107
108            for (IPath p : sourceFolders) {
109                // check if we are inside one of those source class path
110
111                // get the segments
112                String[] srcSegments = p.segments();
113
114                // compare segments. We want the path of the resource
115                // we're visiting to be
116                boolean valid = true;
117                int segmentCount = pathSegments.length;
118
119                for (int i = 0 ; i < segmentCount; i++) {
120                    String s1 = pathSegments[i];
121                    String s2 = srcSegments[i];
122
123                    if (s1.equalsIgnoreCase(s2) == false) {
124                        valid = false;
125                        break;
126                    }
127                }
128
129                if (valid) {
130                    // this folder, or one of this children is a source
131                    // folder!
132                    // we return its segments
133                    return srcSegments;
134                }
135            }
136
137            return null;
138        }
139
140        /**
141         * Sent when an XML error is detected.
142         * @see XmlErrorListener
143         */
144        @Override
145        public void errorFound() {
146            mXmlError = true;
147        }
148    }
149
150    protected static class AbortBuildException extends Exception {
151        private static final long serialVersionUID = 1L;
152    }
153
154    public BaseBuilder() {
155        super();
156        mParserFactory = SAXParserFactory.newInstance();
157
158        // FIXME when the compiled XML support for namespace is in, set this to true.
159        mParserFactory.setNamespaceAware(false);
160    }
161
162    /**
163     * Checks an Xml file for validity. Errors/warnings will be marked on the
164     * file
165     * @param resource the resource to check
166     * @param visitor a valid resource delta visitor
167     */
168    protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) {
169
170        // first make sure this is an xml file
171        if (resource instanceof IFile) {
172            IFile file = (IFile)resource;
173
174            // remove previous markers
175            removeMarkersFromResource(file, AdtConstants.MARKER_XML);
176
177            // create  the error handler
178            XmlErrorHandler reporter = new XmlErrorHandler(file, visitor);
179            try {
180                // parse
181                getParser().parse(file.getContents(), reporter);
182            } catch (Exception e1) {
183            }
184        }
185    }
186
187    /**
188     * Returns the SAXParserFactory, instantiating it first if it's not already
189     * created.
190     * @return the SAXParserFactory object
191     * @throws ParserConfigurationException
192     * @throws SAXException
193     */
194    protected final SAXParser getParser() throws ParserConfigurationException,
195            SAXException {
196        return mParserFactory.newSAXParser();
197    }
198
199    /**
200     * Adds a marker to the current project.  This methods catches thrown {@link CoreException},
201     * and returns null instead.
202     *
203     * @param markerId The id of the marker to add.
204     * @param message the message associated with the mark
205     * @param severity the severity of the marker.
206     * @return the marker that was created (or null if failure)
207     * @see IMarker
208     */
209    protected final IMarker markProject(String markerId, String message, int severity) {
210        return BaseProjectHelper.markResource(getProject(), markerId, message, severity);
211    }
212
213    /**
214     * Removes markers from a resource and only the resource (not its children).
215     * @param file The file from which to delete the markers.
216     * @param markerId The id of the markers to remove. If null, all marker of
217     * type <code>IMarker.PROBLEM</code> will be removed.
218     */
219    public final void removeMarkersFromResource(IResource resource, String markerId) {
220        try {
221            if (resource.exists()) {
222                resource.deleteMarkers(markerId, true, IResource.DEPTH_ZERO);
223            }
224        } catch (CoreException ce) {
225            String msg = String.format(Messages.Marker_Delete_Error, markerId, resource.toString());
226            AdtPlugin.printErrorToConsole(getProject(), msg);
227        }
228    }
229
230    /**
231     * Removes markers from a container and its children.
232     * @param folder The container from which to delete the markers.
233     * @param markerId The id of the markers to remove. If null, all marker of
234     * type <code>IMarker.PROBLEM</code> will be removed.
235     */
236    protected final void removeMarkersFromContainer(IContainer folder, String markerId) {
237        try {
238            if (folder.exists()) {
239                folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE);
240            }
241        } catch (CoreException ce) {
242            String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString());
243            AdtPlugin.printErrorToConsole(getProject(), msg);
244        }
245    }
246
247    /**
248     * Get the stderr output of a process and return when the process is done.
249     * @param process The process to get the ouput from
250     * @param stdErr The array to store the stderr output
251     * @return the process return code.
252     * @throws InterruptedException
253     */
254    protected final int grabProcessOutput(final Process process,
255            final ArrayList<String> stdErr) throws InterruptedException {
256        return BuildHelper.grabProcessOutput(getProject(), process, stdErr);
257    }
258
259
260
261    /**
262     * Saves a String property into the persistent storage of the project.
263     * @param propertyName the name of the property. The id of the plugin is added to this string.
264     * @param value the value to save
265     * @return true if the save succeeded.
266     */
267    protected boolean saveProjectStringProperty(String propertyName, String value) {
268        IProject project = getProject();
269        return ProjectHelper.saveStringProperty(project, propertyName, value);
270    }
271
272
273    /**
274     * Loads a String property from the persistent storage of the project.
275     * @param propertyName the name of the property. The id of the plugin is added to this string.
276     * @return the property value or null if it was not found.
277     */
278    protected String loadProjectStringProperty(String propertyName) {
279        IProject project = getProject();
280        return ProjectHelper.loadStringProperty(project, propertyName);
281    }
282
283    /**
284     * Saves a property into the persistent storage of the project.
285     * @param propertyName the name of the property. The id of the plugin is added to this string.
286     * @param value the value to save
287     * @return true if the save succeeded.
288     */
289    protected boolean saveProjectBooleanProperty(String propertyName, boolean value) {
290        IProject project = getProject();
291        return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value));
292    }
293
294    /**
295     * Loads a boolean property from the persistent storage of the project.
296     * @param propertyName the name of the property. The id of the plugin is added to this string.
297     * @param defaultValue The default value to return if the property was not found.
298     * @return the property value or the default value if the property was not found.
299     */
300    protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) {
301        IProject project = getProject();
302        return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue);
303    }
304
305    /**
306     * Aborts the build if the SDK/project setups are broken. This does not
307     * display any errors.
308     *
309     * @param javaProject The {@link IJavaProject} being compiled.
310     * @param projectState the project state, optional. will be queried if null.
311     * @throws CoreException
312     */
313    protected void abortOnBadSetup(@NonNull IJavaProject javaProject,
314            @Nullable ProjectState projectState) throws AbortBuildException, CoreException {
315        IProject iProject = javaProject.getProject();
316        // check if we have finished loading the project target.
317        Sdk sdk = Sdk.getCurrent();
318        if (sdk == null) {
319            throw new AbortBuildException();
320        }
321
322        if (projectState == null) {
323            projectState = Sdk.getProjectState(javaProject.getProject());
324        }
325
326        // get the target for the project
327        IAndroidTarget target = projectState.getTarget();
328
329        if (target == null) {
330            throw new AbortBuildException();
331        }
332
333        // check on the target data.
334        if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) {
335            throw new AbortBuildException();
336       }
337
338        mBuildToolInfo = projectState.getBuildToolInfo();
339        if (mBuildToolInfo == null) {
340            mBuildToolInfo = sdk.getLatestBuildTool();
341
342            if (mBuildToolInfo == null) {
343                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
344                        "No \"Build Tools\" package available; use SDK Manager to install one.");
345                throw new AbortBuildException();
346            } else {
347                AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
348                        String.format("Using default Build Tools revision %s",
349                                mBuildToolInfo.getRevision())
350                        );
351            }
352        }
353
354        // abort if there are TARGET or ADT type markers
355        stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO,
356                false /*checkSeverity*/);
357        stopOnMarker(iProject, AdtConstants.MARKER_ADT, IResource.DEPTH_ZERO,
358                false /*checkSeverity*/);
359    }
360
361    protected void stopOnMarker(IProject project, String markerType, int depth,
362            boolean checkSeverity)
363            throws AbortBuildException {
364        try {
365            IMarker[] markers = project.findMarkers(markerType, false /*includeSubtypes*/, depth);
366
367            if (markers.length > 0) {
368                if (checkSeverity == false) {
369                    throw new AbortBuildException();
370                } else {
371                    for (IMarker marker : markers) {
372                        int severity = marker.getAttribute(IMarker.SEVERITY, -1 /*defaultValue*/);
373                        if (severity == IMarker.SEVERITY_ERROR) {
374                            throw new AbortBuildException();
375                        }
376                    }
377                }
378            }
379        } catch (CoreException e) {
380            // don't stop, something's really screwed up and the build will break later with
381            // a better error message.
382        }
383    }
384
385    /**
386     * Handles a {@link StreamException} by logging the info and marking the project.
387     * This should generally be followed by exiting the build process.
388     *
389     * @param e the exception
390     */
391    protected void handleStreamException(StreamException e) {
392        IAbstractFile file = e.getFile();
393
394        String msg;
395
396        IResource target = getProject();
397        if (file instanceof IFileWrapper) {
398            target = ((IFileWrapper) file).getIFile();
399
400            if (e.getError() == StreamException.Error.OUTOFSYNC) {
401                msg = "File is Out of sync";
402            } else {
403                msg = "Error reading file. Read log for details";
404            }
405
406        } else {
407            if (e.getError() == StreamException.Error.OUTOFSYNC) {
408                msg = String.format("Out of sync file: %s", file.getOsLocation());
409            } else {
410                msg = String.format("Error reading file %s. Read log for details",
411                        file.getOsLocation());
412            }
413        }
414
415        AdtPlugin.logAndPrintError(e, getProject().getName(), msg);
416        BaseProjectHelper.markResource(target, AdtConstants.MARKER_ADT, msg,
417                IMarker.SEVERITY_ERROR);
418    }
419
420    /**
421     * Handles a generic {@link Throwable} by logging the info and marking the project.
422     * This should generally be followed by exiting the build process.
423     *
424     * @param t the {@link Throwable}.
425     * @param message the message to log and to associate with the marker.
426     */
427    protected void handleException(Throwable t, String message) {
428        AdtPlugin.logAndPrintError(t, getProject().getName(), message);
429        markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
430    }
431
432    /**
433     * Recursively delete all the derived resources from a root resource. The root resource is not
434     * deleted.
435     * @param rootResource the root resource
436     * @param monitor a progress monitor.
437     * @throws CoreException
438     *
439     */
440    protected void removeDerivedResources(IResource rootResource, IProgressMonitor monitor)
441            throws CoreException {
442        removeDerivedResources(rootResource, false, monitor);
443    }
444
445    /**
446     * delete a resource and its children. returns true if the root resource was deleted. All
447     * sub-folders *will* be deleted if they were emptied (not if they started empty).
448     * @param rootResource the root resource
449     * @param deleteRoot whether to delete the root folder.
450     * @param monitor a progress monitor.
451     * @throws CoreException
452     */
453    private void removeDerivedResources(IResource rootResource, boolean deleteRoot,
454            IProgressMonitor monitor) throws CoreException {
455        if (rootResource.exists()) {
456            // if it's a folder, delete derived member.
457            if (rootResource.getType() == IResource.FOLDER) {
458                IFolder folder = (IFolder)rootResource;
459                IResource[] members = folder.members();
460                boolean wasNotEmpty = members.length > 0;
461                for (IResource member : members) {
462                    removeDerivedResources(member, true /*deleteRoot*/, monitor);
463                }
464
465                // if the folder had content that is now all removed, delete the folder.
466                if (deleteRoot && wasNotEmpty && folder.members().length == 0) {
467                    rootResource.getLocation().toFile().delete();
468                }
469            }
470
471            // if the root resource is derived, delete it.
472            if (rootResource.isDerived()) {
473                rootResource.getLocation().toFile().delete();
474            }
475        }
476    }
477
478    protected void launchJob(Job newJob) {
479        newJob.setPriority(Job.BUILD);
480        newJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
481        newJob.schedule();
482    }
483}
484