package org.vaadin.mideaas.model;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactoryConfigurationError;
import org.apache.commons.io.FileUtils;
import org.vaadin.chatbox.SharedChat;
import org.vaadin.mideaas.frontend.MavenUtil;
import org.vaadin.mideaas.frontend.PomXml;
import org.vaadin.mideaas.frontend.PomXml.Dependency;
import org.vaadin.mideaas.java.util.CompilingService;
import org.xml.sax.SAXException;
import com.vaadin.ui.Notification;
/**
* A MIDEaaS project.
*
* Project contains mainly {@link ProjectFile}s and {@link SharedView}s.
*
* It also keeps track of the {@link User}s in the project.
*
* Thread-safe. (should be)
*/
public class SharedProject {
/**
* For notifying on project structure changes. (File add/remove etc.)
*/
public interface ProjectListener {
// TODO: maybe multiple methods, not just one all-purpose "changed"
public void changed();
}
/**
* For notifying when the project classpath changes.
*
*/
public interface ClasspathListener {
public void classpathChanged();
}
// CopyOnWriteArrayList requires no synchronization.
// We may miss a listener call at some point put not a big deal.
private final CopyOnWriteArrayList<ProjectListener> listeners = new CopyOnWriteArrayList<ProjectListener>();
private final CopyOnWriteArrayList<ClasspathListener> cpListeners = new CopyOnWriteArrayList<ClasspathListener>();
/**
* The dir where all the projects are saved.
*/
private static File projectRootDir;
/**
* Projects package is projectRootPackage+"."+projectName
*/
private static String projectRootPackage;
/*
* All the projects.
* Must use synchronized(projects) when accessing.
*/
private static TreeMap<String,SharedProject> projects = new TreeMap<String,SharedProject>();
private static Set<String> projectNames;
/** The users currently editing this project. */
private final TreeSet<User> users = new TreeSet<User>();
/** The name of this project. */
private final String name;
/** The java package where the source files are. */
private final String packageName;
/** Where this project is saved. */
private final File projectDir;
private TreeMap<String, ProjectItem> projectItems = new TreeMap<String, ProjectItem>();
private PomXml pomXml;
private String classPath;
/** Project-wide chat. */
private SharedChat chat = new SharedChat();
private CompilingService compiler;
private final ProjectLog log;
/**
* Sets the static project properties such as root dir and package.
* This must be called before doing anything with SharedProjects.
*
* @throws IOException
*/
// TODO: how should we synchronize the static fields?
public static void initializeProjectRoot(File projectRootDir,
String projectRootPackage) throws IOException {
SharedProject.projectRootDir = projectRootDir;
SharedProject.projectRootPackage = projectRootPackage;
// TODO: it's not a very good idea to read all the projects
// to memory right away. because we're most likely gonna need
// many of them...
loadProjectlistFromFiles(projectRootDir);
}
private static String getProjectPackageFor(String projectName) {
//System.out.println("1: " + projectRootPackage);
//System.out.println("2: " + projectPackageName(projectName));
return projectRootPackage + "." + projectPackageName(projectName);
}
private static String projectPackageName(String projectName) {
return projectName.toLowerCase().replaceAll("[^a-z0-9]", "");
}
static private File createNewProjectDir(String projectName)
throws IOException {
File dir = new File(projectRootDir, projectName);
dir.mkdirs();
return dir;
}
/**
* Creates a new project without any files.
*/
public static SharedProject createEmptyProject(String name) {
SharedProject s;
File projectDir;
synchronized (projects) {
if (SharedProject.getProjectNames().contains(name)) {
return null;
}
try {
projectDir = createNewProjectDir(name);
s = new SharedProject(name, projectDir);
} catch (IOException e) {
e.printStackTrace();
removeProject(name);
return null;
}
putProject(s);
}
return s;
}
/**
* Creates a new project with default initial files.
*/
public static SharedProject createNewProject(String name, UserSettings settings) {
SharedProject s = createEmptyProject(name);
if (s == null) {
return null;
}
try {
s.log.logCreated();
ProjectFileUtils.writeInitialFilesToDisk(s.getProjectDir(),
s.getPackageName(), settings);
s.pomXml = new PomXml(ProjectFileUtils.generatePomXml(s
.getPackageName(),settings));
s.writePomXml();
s.addUiClass();
s.addHelloWorldSkeleton();
s.writeToDisk();
s.compileAll();
} catch (IOException | ParserConfigurationException | SAXException
| TransformerFactoryConfigurationError | TransformerException e) {
e.printStackTrace();
Notification.show("Error: failed creating project: " + name,
Notification.Type.ERROR_MESSAGE);
removeProject(name);
return null;
}
return s;
}
private void addUiClass() {
String code = ProjectFileUtils.generateApp(getPackageName());
ProjectFile pf = ProjectFile.newJavaFile("App.java", code, getSourceFileLocation("App.java"), getLog());
addFile(pf, null);
}
/**
* Compiles all the files of this project.
* Normally a file is compiled after it's changed but this
* may be needed in some situations. (?)
*/
public synchronized void compileAll() {
getCompiler().compileAll(getJavaClasses());
}
/**
* @return map<java class name, the class java code>
*/
private Map<String, String> getJavaClasses() {
Map<String,String> classes = new HashMap<String,String>();
for (ProjectItem po : projectItems.values()) {
String[] cls = po.getJavaClass();
if (cls!=null) {
classes.put(getPackageName()+"."+cls[0], cls[1]);
}
}
// Assuming every project has this MideaasComponent class...
// TODO: this is a bit of a hack...
String cls = "org.vaadin.mideaas.MideaasComponent";
try {
String content = ProjectFileUtils.generateMideaasComponent();
classes.put(cls, content);
} catch (IOException e) {
System.err.println("WARNING: could not compile MideaasComponent");
e.printStackTrace();
}
return classes;
}
private String fullJavaClassNameFromFilename(String name) {
return getPackageName()+"."+name.substring(0, name.length() - ".java".length());
}
/**
* Removes the project and directory.
*
* @param projectName
* the name of the project to be destroyer
* @return true, if successful
*/
public static boolean removeProject(String projectName) {
SharedProject project = SharedProject.getProject(projectName);
return removeProject(project);
}
/**
* Removes the project and directory.
*
* @param project
* to be destroyer
* @return true, if successful
*/
private static boolean removeProject(SharedProject project) {
if (project != null) {
project.destroy();
synchronized (projects) {
projects.remove(project.getName());
projectNames.remove(project.getName());
}
project.log.logRemoved();
} else {
return false;
}
return true;
}
private void destroy() {
removeAllUsers();
try {
FileUtils.deleteDirectory(getProjectDir());
} catch (IOException e) {
// TODO what?
e.printStackTrace();
}
}
private void addHelloWorldSkeleton() {
createView(ProjectFileUtils.getFirstViewName(), null);
}
/**
* Instantiates a new shared project.
*
* Private because
* {@link #createEmptyProject(String)} or
* {@link #createNewProject(String)}
* should be used instead.
*
* @param projectName
* the project name
* @param projectDir
* the project dir
* @throws IOException
* Signals that an I/O exception has occurred.
*/
private SharedProject(String projectName, File projectDir)
throws IOException {
this.name = projectName;
this.projectDir = projectDir;
this.packageName = getProjectPackageFor(projectName);
log = new ProjectLog(projectName);
chat.addLine("Project " + projectName + " started.");
}
/**
* @return the java classpath.
*/
public synchronized String getClassPath() {
if (classPath == null) {
classPath = ProjectFileUtils.getClassPath(projectDir);
}
return classPath;
}
public synchronized void addListener(ProjectListener li) {
listeners.add(li);
}
public synchronized void removeListener(ProjectListener li) {
listeners.remove(li);
}
public synchronized void addClasspathListener(ClasspathListener li) {
cpListeners.add(li);
}
public synchronized void removeClasspathListener(ClasspathListener li) {
cpListeners.remove(li);
}
private void fireChanged() {
for (ProjectListener listener : listeners) {
listener.changed();
}
}
private void recheckClasspath() {
classPath = null;
for (ClasspathListener listener : cpListeners) {
listener.classpathChanged();
}
}
/**
* @return the location of pom.xml file on disk.
*/
public File getPomXmlFile() {
return ProjectFileUtils.getPomXmlFile(projectDir);
}
/**
* Creates a new view.
*
* @param name
* @param byUser
* @return the view; null if failed.
*/
public SharedView createView(String name, User byUser) {
if (!SharedView.isvalidName(name)) {
System.err.println(name + " is not a valid name.");
return null;
}
SharedView c = null;
synchronized (this) {
if (containsProjectItem(name)) {
System.err.println("Project object '" + name + "' already exists.");
return null;
}
c = new SharedView(getPackageName(), name, getSourceDir(), log);
addView(c, byUser);
}
fireChanged();
recheckClasspath();
return c;
}
/**
* The project's name.
*/
public String getName() {
// No sync because final.
return name;
}
/**
* The java package of the project.
*/
public String getPackageName() {
// No sync because final.
return packageName;
}
/**
* Writes all the project files to the disk.
*
* @throws IOException
*/
public void writeToDisk() throws IOException {
writeToDisk(getProjectDir());
}
public synchronized void writeToDisk(File dir) throws IOException {
File src = ProjectFileUtils.getSourceDir(dir, getPackageName());
for (ProjectItem po : projectItems.values()) {
po.writeBaseToDisk(src);
}
try {
writePomXml(dir);
} catch (TransformerFactoryConfigurationError | TransformerException e) {
throw new IOException(e);
}
}
public synchronized void writeToDiskIncludingInitial(File dir, UserSettings settings) throws IOException {
ProjectFileUtils.writeInitialFilesToDisk(dir, getPackageName(), settings);
writeToDisk(dir);
}
/**
* The dir where the projects files are saved.
*/
public File getProjectDir() {
return projectDir;
}
/**
* The dir where all the .java and other source files of this project are.
* Eg. <projectDir>/src/main/java/com/arvue/apps/<projectName>/
*/
private File getSourceDir() {
return ProjectFileUtils.getSourceDir(getProjectDir(), getPackageName());
}
private void writePomXml(File dir) throws IOException,
TransformerFactoryConfigurationError, TransformerException {
String s = pomXml.getAsString();
ProjectFileUtils.writePomXml(dir, s);
}
private void writePomXml() throws IOException,
TransformerFactoryConfigurationError, TransformerException {
writePomXml(getProjectDir());
}
/**
* Reads the projects contents from disk.
*
* TODO XXX: This probably works only once, when the project is empty???
*/
public void refreshFromDisk() {
try {
synchronized (this) {
Map<String, String> srcFiles = ProjectFileUtils.readSourceFiles(
projectDir, this.getPackageName());
setSourceFiles(srcFiles);
this.pomXml = new PomXml(ProjectFileUtils.readPomXml(projectDir));
}
} catch (IOException | ParserConfigurationException | SAXException e) {
System.err.println("WARNING: could not refresh from disk: "
+ e.getMessage());
e.printStackTrace();
}
// I think it's the right thing to do to compile all after refresh. (?)
compileAll();
// The project may have not have always changed
// but too lazy to actually check that...
fireChanged();
}
/**
* Sets the source files.
*
* XXX.java files with corresponding XXX.clara.xml are treated as views.
*
*/
private void setSourceFiles(Map<String, String> srcFiles) {
HashMap<String, String> claraXmls = new HashMap<String, String>();
HashMap<String, String> files = new HashMap<String, String>();
for (Entry<String, String> e : srcFiles.entrySet()) {
if (e.getKey().endsWith(".clara.xml")) {
claraXmls.put(e.getKey(), e.getValue());
} else if (isEditableFile(e.getKey())) {
files.put(e.getKey(), e.getValue());
}
}
TreeMap<String, SharedView> views = new TreeMap<String, SharedView>();
TreeMap<String, ProjectFile> projFiles = new TreeMap<String, ProjectFile>();
for (Entry<String, String> e : files.entrySet()) {
String n = e.getKey();
if (n.endsWith(".java")) {
String vn = n.substring(0, n.length() - 5);
String xmlName = vn + ".clara.xml";
if (claraXmls.containsKey(xmlName)) {
SharedView v = new SharedView(getPackageName(), vn, getSourceDir(), log);
v.setControllerBase(e.getValue());
v.setModelBase(claraXmls.get(xmlName));
views.put(vn, v);
} else {
File saveTo = new File(getSourceDir(), n);
projFiles.put(n, ProjectFile.newJavaFile(n, e.getValue(), saveTo, log));
}
} else {
File saveTo = new File(getSourceDir(), n);
projFiles.put(n, new ProjectFile(n, e.getValue(), null, saveTo, log));
}
}
projectItems = new TreeMap<String, ProjectItem>(views);
projectItems.putAll(projFiles);
}
/**
* @return true iff the users are supposed to edit this file.
*/
private static boolean isEditableFile(String name) {
// TODO: what?
return true;
//return !name.equals(ProjectFileUtils.getAppClassName() + ".java");
}
public synchronized ProjectItem getProjectItem(String name) {
return projectItems.get(name);
}
public synchronized boolean containsProjectItem(String name) {
return projectItems.containsKey(name);
}
// public synchronized List<String> getViewNames() {
// LinkedList<String> names = new LinkedList<String>();
// for (ProjectItem po : projectItems.values()) {
// if (po instanceof SharedView) {
// names.add(po.getName());
// }
// }
// return names;
// }
//
// public synchronized List<String> getFileNames() {
// LinkedList<String> names = new LinkedList<String>();
// for (ProjectItem po : projectItems.values()) {
// if (po instanceof ProjectFile) {
// names.add(po.getName());
// }
// }
// return names;
// }
public synchronized List<ProjectItem> getProjectItemsCopy() {
return new LinkedList<ProjectItem>(projectItems.values());
}
public synchronized List<Dependency> getDependencies() {
// Defensive copy because we don't want others to mess with the contents.
return new LinkedList<Dependency>(pomXml.getDependencies());
}
/**
* Sets a new Maven dependency.
*
* Takes an xml snippet such as:
* <dependency>
* <groupId>org.vaadin.addons</groupId>
* <artifactId>aceeditor</artifactId>
* <version>0.8.2</version>
* </dependency>
*
* @param xmlSnippet
* @throws UnsupportedEncodingException
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
* @throws TransformerFactoryConfigurationError
* @throws TransformerException
*/
public void addDependency(String xmlSnippet)
throws UnsupportedEncodingException, ParserConfigurationException,
SAXException, IOException, TransformerFactoryConfigurationError, TransformerException {
synchronized (this) {
pomXml.addDependency(xmlSnippet);
writePomXml();
}
recheckClasspath();
fireChanged(); // TODO should we fire this changed, too(?)
}
public void removeDependency(Dependency dep) {
synchronized (this) {
pomXml.removeDependency(dep);
}
recheckClasspath();
fireChanged();
// TODO Auto-generated method stub
}
/**
* Gets the list of the users in the project.
*
* @return the names of collaborators separated with comma
*/
public synchronized List<User> getUsers() {
return new ArrayList<User>(users);
}
/**
* Adds new collaborator.
*
* @param user
* the user
*/
public void addUser(User user) {
boolean added;
synchronized (this) {
added = users.add(user);
}
if (added) {
getChat().addLine(user.getName() + " joined");
// TODO: should we fire some changed event?
}
}
private void removeAllUsers() {
@SuppressWarnings("unused")
Collection<User> removed;
synchronized (this) {
removed = new TreeSet<User>(users);
users.clear();
}
// TODO fire users removed!
}
/**
* Removes user from all of the projects.
*
* @param user
* the user to be removed from projects
*/
public static void removeFromProjects(User user) {
synchronized (projects) {
for (SharedProject project : projects.values()) {
project.removeUser(user);
}
}
}
/**
* Checks if user is currently in any project.
*
* @param user
* @return true, if user is collaborating in one of the projects
*/
public static boolean isInProject(User user) {
if (user == null) {
return false;
}
synchronized (projects) {
for (SharedProject project : projects.values()) {
if (project.hasUser(user)) {
return true;
}
}
}
return false;
}
/**
* Checks if user is in this project.
*
* @param user
* @return true, if user is collaborating in this project
*/
public synchronized boolean hasUser(User user) {
if (user == null) {
return false;
} else {
return users.contains(user);
}
}
/**
* Gets a project by name.
*
* @return the project; null if no such project
*/
public static SharedProject getProject(String name) {
synchronized (projects) {
SharedProject p = projects.get(name);
if (p!=null) {
return p;
}
else {
if (projectNames.contains(name)) {
try {
// XXX This may take a long time, so shouldn't be inside synchronized...
return loadProject(name);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
return null;
}
private static SharedProject loadProject(String name) throws IOException {
SharedProject p = loadProjectFromFiles(new File(projectRootDir, name));
synchronized (projects) {
projects.put(name, p);
}
return p;
}
/**
* Gets the project names.
*
* @return the project names
*/
public static Collection<String> getProjectNames() {
synchronized (projects) {
return new TreeSet<String>(projectNames);
//return new TreeSet<String>(projects.keySet());
}
}
/**
* Load projects from disk.
*
* @return the linked list
* @throws IOException
*/
private static void loadProjectlistFromFiles(
File projectRoot) throws IOException {
synchronized (projects) {
if (projectRoot.exists()) {
projectNames = new HashSet<String>();
for (File projectDir : projectRoot.listFiles()) {
projectNames.add(projectDir.getName());
}
} else {
throw new FileNotFoundException(projectRoot + " not found!");
}
}
}
/**
* Reads the project from the file location and adds it into the list of projects.
*
* @param projectDir
* the project dir
* @throws IOException
*/
public static void addProjectFromFiles(File projectDir) throws IOException {
putProject(loadProjectFromFiles(projectDir));
}
/**
* Adds the project.
*/
private static void putProject(SharedProject p) {
synchronized (projects) {
projects.put(p.getName(), p);
projectNames.add(p.getName());
}
}
/**
* Load project from files.
*
* @param projectDir
* the project dir
* @return the shared project
* @throws IOException
*/
private static SharedProject loadProjectFromFiles(File projectDir)
throws IOException {
if (!projectDir.isDirectory()) {
throw new IllegalArgumentException("No such dir: " + projectDir);
}
SharedProject s = new SharedProject(projectDir.getName(), projectDir);
s.refreshFromDisk();
s.log.logLoadedFromDisk(projectDir);
return s;
}
/**
* Returns the project chat.
*/
public SharedChat getChat() {
return chat;
}
/**
* The java compiler of the project.
*/
public synchronized CompilingService getCompiler() {
if (compiler == null) {
compiler = new CompilingService(this);
}
return compiler;
}
private void addView(SharedView view, User byUser) {
projectItems.put(view.getName(), view);
getCompiler().compile(view);
if (byUser!=null) {
getChat().addLine(byUser.getName() + " created a view: " + view.getName());
}
}
public boolean addFile(ProjectFile f, User byUser) {
synchronized (this) {
if (containsProjectItem(f.getName())) {
return false;
}
projectItems.put(f.getName(), f);
}
compileFile(f);
if (byUser!=null) {
getChat().addLine(byUser.getName() + " created a file: " + f.getName());
}
fireChanged();
return true;
}
private void compileFile(ProjectFile f) {
if (f.getName().endsWith(".java")) {
String cls = fullJavaClassNameFromFilename(f.getName());
getCompiler().compile(cls, f.getBaseText(), null);
}
}
public void removeProjectItem(String name, User byUser) {
ProjectItem removed;
synchronized (this) {
removed = projectItems.remove(name);
}
// TODO: remove from git repository!
if (removed!=null) {
removed.removeFromDir(getSourceDir());
removed.removeFromClasspathOf(getCompiler(), getPackageName());
getChat().addLine(byUser.getName() + " deleted " + removed.getName());
fireChanged();
}
}
public File getSourceFileLocation(String filename) {
return new File(getSourceDir(), filename);
}
public ProjectLog getLog() {
// No need to sync because final.
return log;
}
public static List<User> getProjectUsers(String projectName) {
synchronized (projects) {
if (projects.containsKey(projectName)) {
return new ArrayList<User>(projects.get(projectName).getUsers());
}
}
return Collections.emptyList();
}
public static boolean projectExists(String name) {
return getProjectNames().contains(name);
}
public void removeUser(User user) {
boolean removed;
synchronized (this) {
removed = users.remove(user);
for (ProjectItem po : projectItems.values()) {
po.removeUser(user);
}
}
if (removed) {
getChat().addLine(user.getName() + " left");
}
}
public File getTargetPathFor(User u) {
//System.out.println( "absolute path of project directory: " + this.projectDir.getAbsolutePath());
return new File(this.getProjectDir().getAbsolutePath(), MavenUtil.targetDirFor(u));
}
}