WebsiteProject.java

/**
 * 
 */
package edu.odu.cs.cowem.documents;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Directory layout of a website project. Allows easy access
 * to groups, document sets, and the generation of relative
 * paths among them.
 *  
 * @author zeil
 *
 */
public class WebsiteProject implements Iterable<String> {
  
    
    /**
     * For logging error messages.
     */
    private static Logger logger 
       = LoggerFactory.getLogger(WebsiteProject.class);
    

    /**
     * Name used for Gradle files in document set directories.
     */
    private static final String GRADLE_FILE_NAME = "build.gradle";


    /**
     * Short name of the the course, converted to a URL-safe string.
     */
    private String courseName;
    
    /**
     * Root directory of the project.
     */
    private File rootDir;
    
    /**
     * Mapping from all known document set names to their
     * containing directories.
     */
    private Map<String, File> documentSets;
    
    /**
     * Mapping from all known document set names to their
     * source code file (.md or .mmd).
     */
    private Map<String, File> documentSources;

    /**
     * Mapping from all known document set names to their
     * titles.
     */
    private Map<String, String> documentTitles;
    
    /**
     * Map from imported site names to their URLs 
     */
    private Map<String, String> importedSites; 
    
    

    /**
     * Create a project summary object.
     * @param rootDirectory location of this project's root
     */
    public WebsiteProject (final File rootDirectory) {
        rootDir = rootDirectory.getAbsoluteFile();
        documentSets = new TreeMap<String,File>();
        documentSources = new TreeMap<String,File>();
        documentTitles = new TreeMap<String,String>();
        courseName = "unknown";
        
        for (File group: rootDir.listFiles()) {
            if (group.isDirectory()) {
                rememberDocumentSetsInThisGroup(group);
            }
        }
    }


	private void rememberDocumentSetsInThisGroup(File group) {
		for (File docSet: group.listFiles()) {
		    if (docSet.isDirectory() 
		            && new File(docSet, GRADLE_FILE_NAME).exists()) {
		        // This is a valid document set
		    	rememberDocumentSetComponents(docSet);
		    }
		}
	}


	private void rememberDocumentSetComponents(File docSet) {
		String docSetName = docSet.getName();
		warnIfDocSetNameIsDuplicated(docSetName);
		documentSets.put(docSetName, 
		        docSet.getAbsoluteFile());
		//logger.warn("Remember doc set " + docSet.getName() + " at " + docSet.getAbsoluteFile());
		File sourceFile = new File(docSet, docSetName + ".md");
		rememberThisDocument(docSetName, sourceFile);
		scanForSecondaryDocuments(docSet);
	}


	private void scanForSecondaryDocuments(File docSet) {
		for (File componentFile: docSet.listFiles()) {
			String fileName = componentFile.getName();
			if (fileName.endsWith(".mmd")) {
				// This is a secondary document.
				warnIfDocSetNameIsDuplicated(fileName);
				documentSets.put(fileName, docSet);
				rememberThisDocument(fileName, componentFile);
			}
		}
	}


	private void warnIfDocSetNameIsDuplicated(String docSetName) {
		if (documentSets.containsKey(docSetName)) {
		    logger.warn (
		       "Ambiguous structure: two or more document sets named "
		            + docSetName);
		}
	}


	private void rememberThisDocument(String docSetName, File sourceFile) {
		if (sourceFile.exists()) {
			documentSources.put(docSetName, sourceFile);
			String title = extractTitleFrom(sourceFile);
			if (title != null) {
				documentTitles.put(docSetName, title);
			}
				
		}
	}
    
    
    private String extractTitleFrom(File sourceFile) {
		try (BufferedReader in = new BufferedReader(new FileReader(sourceFile))) {
			String line = in.readLine();
			while (line != null) {
				if (line.toLowerCase().startsWith("title:")) {
					int k = 6;
					while (k < line.length() && line.charAt(k) == ' ') {
						++k;
					}
					String title = line.substring(k);
					return title;
				}
				line = in.readLine();
			}
		} catch (IOException e) {
			logger.warn ("Unexpected problem getting title from " + sourceFile, e);
		}
		return null;
	}


	/**
     * Determine the relative path from some file to the project root.
     * @param from a file that is a descendant of the project root.
     * @return the relative path from some file to the project root or null
     *            if no such relation can be determined.
     */
    public final Path relativePathToRoot (final File from) {
        File aFrom = from.getAbsoluteFile();
        if (!aFrom.isDirectory()) {
            aFrom = aFrom.getParentFile();
        }
        Path p = aFrom.toPath().relativize(rootDir.toPath());
        return p;
    }
    
    /**
     * Compute a relative path from a file to the directory containing
     * some document set.
     * @param from  a file that is descended from the project root.
     * @param documentSet any document set
     * @return the relative path, or null if no such path can be determined
     *    (the file is not descended from the project root or the document
     *      set does not exist). 
     */
    public final Path relativePathToDocumentSet (final File from, 
            final String documentSet) {
        File docSet = documentSetLocation(documentSet);
        if (docSet == null) {
            return null;
        }
        Path p1 = relativePathToRoot(from);
        if (p1 == null) {
            return null;
        }
        Path p2 = rootDir.toPath().relativize(docSet.toPath());
        Path p3 = p1.resolve(p2);
        return p3;
        
    }
    
    /**
     * The location of a document set.
     * @param documentSet The name of a document set.
     * @return the location or null if the document set does not exit
     */
    public final File documentSetLocation  (final String documentSet) {
        return documentSets.get(documentSet);
    }

    /**
     * The name of the group to which a document set belongs.
     * @param documentSet name of a document set
     * @return the group name or null if the document set does not exist.
     */
    public final String documentSetGroup (final String documentSet) {
        File loc =  documentSets.get(documentSet);
        if (loc == null) {
            return null;
        }
        return loc.getParentFile().getName();
    }

    /**
     * Iterate through all document sets.
     * @return iterator over the document set names.
     */
    public final Iterator<String> iterator() {
        return documentSets.keySet().iterator();
    }
    
    /**
     * Get the root directory.
     * @return the root directory.
     */
    public final File getRootDir() {
        return rootDir;
    }
    
    /**
     * Printable summary of project.
     * @return summary 
     */
    public final String toString() {
        return rootDir.toString() + ": " + documentSets.keySet().toString();
    }

    /**
     * Source file from which we obtain a named document.
     * @param documentName the identifier for the document (primary or secondary)
     * @return the .md or .mmd file containing the document source
     */
	public File documentSource(String documentName) {
        return documentSources.get(documentName);
	}


    /**
     * Title within a named document.
     * @param documentName the identifier for the document (primary or secondary)
     * @return the contents of the "Title:" metadata line or null if no such line
     */
	public String getDocumentTitle(String documentName) {
		return documentTitles.get(documentName);
	}
    
    /**
     * Gets a mapping from document names to relative URLs in a JSON array
     * format.
     * 
     * @return mapping of known documents to URLs relative to website root
     */
    String getDocumentMap() {
    	StringBuffer result = new StringBuffer();
    	result.append("{ \"mapping\" : \n  [");
    	boolean firstTime = true;
    	for (String docName: documentSources.keySet()) {
    		if (!firstTime) {
    			result.append(",");
    		} else {
    			firstTime = false;
    		}
    		result.append("\n    {\n      \"doc\" : \"");
    		result.append(docName);
    		result.append("\",\n      \"url\" : \"");
    		String docPath = documentSources.get(docName).toString();
    		docPath = docPath.replace('\\', '/');
    		String[] pathParts = docPath.split("/");
    		String file = pathParts[pathParts.length-1];
    		String docSet = pathParts[pathParts.length-2];
    		String docGroup = pathParts[pathParts.length-3];
    		if (file.endsWith(".mmd")) {
    			file = file + ".html";
    		} else {
    			file = "index.html";
    		}
    		docPath = docGroup + "/" + docSet + "/" + file;
    		result.append(docPath);
    		result.append("\"\n    }");
    	}
    	result.append("\n  ]\n}\n");
    	return result.toString();
    }


	public void setImports(Map<String,String> importedSites) {
		this.importedSites = importedSites;
	}


	public String getExternalSite(String siteName) {
		return importedSites.get(siteName);
	}
    
	/**
	 * Save a short name for the course, after replacing any non-URL-safe
	 * characters.
	 * 
	 * @param cname a short name for this course.
	 */
	public void setCourseName (String cname) {
		try {
			courseName = URLEncoder.encode(cname, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			courseName="unknown";
		}
	}
	
	/**
	 * A short name for this course.
	 * 
	 * @return the course name
	 */
	public String getCourseName() {
		return courseName;
	}
}