com.morty.podcast.writer.PodCastGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.morty.podcast.writer.PodCastGenerator.java

Source

// Copyright (c) 2011, Andrew Morton. All rights reserved.
// Use of this source code is governed by a MIT-style license that can be
// found in the LICENSE file.

package com.morty.podcast.writer;

import com.morty.podcast.writer.excpt.PodCastCreationException;
import com.morty.podcast.writer.constants.PodCastConstants;
import com.morty.podcast.writer.file.PodCastFile;
import com.morty.podcast.writer.file.PodCastFileNameResolver;
import com.morty.podcast.writer.file.PodCastFileProperties;
import com.morty.podcast.writer.validation.FolderValidator;
import com.sun.syndication.feed.module.itunes.EntryInformation;
import com.sun.syndication.feed.module.itunes.EntryInformationImpl;
import com.sun.syndication.feed.module.itunes.FeedInformation;
import com.sun.syndication.feed.module.itunes.FeedInformationImpl;
import com.sun.syndication.feed.module.itunes.types.Duration;
import com.sun.syndication.feed.synd.SyndCategory;
import com.sun.syndication.feed.synd.SyndCategoryImpl;
import com.sun.syndication.feed.synd.SyndContent;
import com.sun.syndication.feed.synd.SyndContentImpl;
import com.sun.syndication.feed.synd.SyndEnclosure;
import com.sun.syndication.feed.synd.SyndEnclosureImpl;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndEntryImpl;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.feed.synd.SyndImage;
import com.sun.syndication.feed.synd.SyndImageImpl;
import com.sun.syndication.io.SyndFeedOutput;
import java.io.File;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This will traverse a directory and create a podcast based on it.
 * This can then be used to create the feed.
 * The structure should be as follows:
 * -Main Directory (suggest podcast201011), which then changes every year.
 *  -ModuleName (no spaces should be in this!)
 *      -MP3    filename should be YYYYMMDD.<UnitNumber>.<Description>.mp3
 *      -PDF    For notes
 *      -MPG    For video
 * 
 *      <UnitNumber> is available as %1 in the template
 *      <Description> is available as %2 in the template ( '_' will be replaced by space)
 *
 *      -Optional Info file (info.txt)
 *
 * This will create a podcast with categories of module name
 * Any file that starts '.' will be ignored
 *
 *
 * Modified 15/02/2011 to use the filename date of YYYYMMDD
 * Modified 25/07/2011 for major refactor and handle UTF8 filenames.
 * Modified September 2011 for new features.
 *
 * Major refactor to allow the custom naming, messaging and everything else.
 * Validation has been implemented, but will probably be used as a once off check, as its not very customisable.
 * Future implementations should use a wrapper around a FileFilter (see other project!)
 *
 * Copyright 2011
 * @author amorton
 */
public class PodCastGenerator {

    private static final Log m_logger = LogFactory.getLog(PodCastGenerator.class);

    //Simple mode is no longer valid - if you want to run in simple mode, use
    //an appropriate supported file structure.
    @Deprecated
    private boolean m_simpleMode = false;

    private String m_fileToCreate = null;
    private String m_directoryToTraverse = null;
    private String m_urlSuffix = "";
    private String m_httpRoot = null;
    private Set<String> m_excludedFolders = new HashSet<String>();
    private PodCastFileNameResolver fileResolver = new PodCastFileNameResolver();

    //Validation
    private FolderValidator m_validator = null;
    private boolean m_failOnValidation = false;

    //Invalid files
    private Map m_ignoredFiles = new HashMap();

    //Override value for the http link for the feed.
    private String m_feedLink = null;

    /**
     * Set the feed link override text
     * @param link
     */
    public void setFeedLink(String link) {
        this.m_feedLink = link;
    }

    /**
     * Set the directory that we are looking at
     * @param dir
     */
    public void setDirectory(String dir) {
        this.m_directoryToTraverse = dir;
    }

    /**
     * Set the file that we are to create (absolute path!)
     * @param file
     */
    public void setFileToCreate(String file) {
        this.m_fileToCreate = file;
    }

    /**
     * Set the root URL (including http://)
     * @param httproot
     */
    public void setHttpRoot(String httproot) {
        this.m_httpRoot = httproot;
    }

    /**
     * Boolean to run in simple mode
     * @param simpleMode
     */
    public void setSimpleMode(boolean simpleMode) {
        this.m_simpleMode = simpleMode;
    }

    /**
     * Add in an optional url suffix
     * @param urlSuffix
     */
    public void setUrlSuffix(String urlSuffix) {
        this.m_urlSuffix = urlSuffix;
    }

    /**
     * Optional Folders
     * @param excludedFolders
     */
    public void setExcludedFolders(Set foldersToExclude) {
        this.m_excludedFolders = foldersToExclude;
    }

    /**
     * Allows for strict processing
     * @param folderValidator
     */
    public void setValidator(FolderValidator fv) {
        this.m_validator = fv;
    }

    /**
    * Allows for processing to be stopped!
    * @param failOnValidation
    */
    public void setFailOnValidation(boolean fov) {
        this.m_failOnValidation = fov;
    }

    /**
     * The main method to be generate a podcast
     */
    public void generatePodCast() {
        try {
            m_logger.info("Starting");
            //verify the parameters
            checkValues();
            //Process the files
            List podcastFiles = processMainDirectory();
            generateCompleteFeed(podcastFiles);

            outputIgnoredFiles();

            m_logger.info("Finished");
        } catch (PodCastCreationException pce) {
            m_logger.error("Custom error", pce);
        } catch (Exception ex) {
            m_logger.error("Unable to Generate Podcast", ex);
        }

    }

    //Check all the variables to be valid.
    private void checkValues() throws PodCastCreationException {
        m_logger.info("Checking variables");

        if (m_fileToCreate == null || m_directoryToTraverse == null || m_urlSuffix == null || m_httpRoot == null)
            throw new PodCastCreationException("Variables not specified");

        //Check the directory exists & is a directory!
        File dirCheck = new File(m_directoryToTraverse);
        if (!dirCheck.exists() || !dirCheck.isDirectory())
            throw new PodCastCreationException("Invalid directory specified");

        m_logger.info("Checked variables");

    }

    private List processMainDirectory() throws Exception {
        m_logger.info("Processing directories");

        List returnList = new ArrayList();

        //Process the directory
        File directory = new File(m_directoryToTraverse);

        if (m_validator != null) {
            m_logger.info("Starting validation");
            m_validator.setExcludedFolders(m_excludedFolders);
            m_validator.setFolderToProcess(directory);
            m_validator.setFailOnError(m_failOnValidation);
            m_validator.process();
            m_logger.info("Finished validation");
        }

        //Allow folder exclusion
        File[] modules = directory.listFiles(new FilenameFilter() {

            public boolean accept(File file, String fileToCheck) {
                m_logger.debug("Checking module [" + fileToCheck + "]");
                //if not in list, then accept. If in list, then reject the folder
                if (m_excludedFolders == null || m_excludedFolders.isEmpty())
                    return true;
                else if (m_excludedFolders.contains(fileToCheck)) {
                    m_logger.debug("Skipping excluded folder [" + fileToCheck + "]");
                    //Put these files in the list with an excluded reason
                    m_ignoredFiles.put(file.getAbsolutePath() + File.separator + fileToCheck,
                            PodCastConstants.FILE_EXCLUDED);
                    return false;
                } else
                    return true;
            }
        });

        for (int i = 0; i < modules.length; i++) {
            if (modules[i].isDirectory()) {
                String category = modules[i].getName();

                m_logger.info("Processing module [" + category + "]");

                File categoryDirectory = new File(modules[i].getAbsolutePath());
                File[] originalFiles = categoryDirectory.listFiles();
                PodCastFile[] podcastFiles = new PodCastFile[originalFiles.length];
                Map parentDirectoryProperties = processInfoFile(categoryDirectory);

                //Convert all the files to PodCastFiles in the array.
                for (int counter = 0; counter < podcastFiles.length; counter++) {
                    //Create a podcast file with a construct of file path (preconverted to UTF8).
                    //That way all files should exist, even if they have weird characters and every getName will
                    //also contain the right characters!
                    podcastFiles[counter] = new PodCastFile(
                            PodCastUtils.convertToUTF8(originalFiles[counter].getAbsolutePath()));
                    podcastFiles[counter].setParentProperties(parentDirectoryProperties);
                    fileResolver.formatFile(podcastFiles[counter]);

                }

                for (int p = 0; p < podcastFiles.length; p++) {
                    if (!podcastFiles[p].isValid()) {
                        m_logger.warn(
                                "Ignoring hidden/unresolved or info file [" + podcastFiles[p].getName() + "]");
                        m_ignoredFiles.put(podcastFiles[p].getAbsolutePath(),
                                podcastFiles[p].getProperty(PodCastFileProperties.INVALID_REASON));

                    } else
                        returnList.add(processFile(podcastFiles[p], category, parentDirectoryProperties));
                }

            } else
                m_logger.info("Skipping Loose File [" + modules[i] + "]");
        }

        return returnList;

    }

    private HashMap processInfoFile(File categoryDirectory) {
        HashMap returnValues = new HashMap();
        //Check for the info file. (overrides the author and description!)
        File infoFile = new File(categoryDirectory + File.separator + PodCastConstants.INFO_FILE);
        if (infoFile.exists()) {
            returnValues = PodCastUtils
                    .parseInfoFile(new File(categoryDirectory + File.separator + PodCastConstants.INFO_FILE));
            m_logger.info("Found info file for [" + categoryDirectory + "]");
        } else
            m_logger.warn("No info file at level [" + categoryDirectory + File.separator
                    + PodCastConstants.INFO_FILE + "]");

        return returnValues;

    }

    private SyndEntry processFile(PodCastFile podCastFile, String category, Map customValues) throws Exception {
        m_logger.info("Processing file [" + podCastFile.getName() + "]");
        Date fileDate = podCastFile.getFileDate();
        String desc = podCastFile.getDescription();

        //Add the entry of the file
        return generateEntry(
                m_httpRoot + "/" + category + "/" + podCastFile.getName(), fileDate, desc, category, PodCastUtils
                        .getMapValue(customValues, PodCastConstants.AUTHOR_KEY, PodCastConstants.DEFAULT_AUTHOR),
                podCastFile, m_urlSuffix);

    }

    private void generateCompleteFeed(List podcastFiles) throws Exception {

        try {

            //Get the default info file from the podcast directory, and see if there are any overrides
            File defaultInfo = new File(m_directoryToTraverse + File.separator + PodCastConstants.INFO_FILE);
            HashMap defaultFeedValues = new HashMap();
            if (defaultInfo.exists())
                defaultFeedValues = PodCastUtils.parseInfoFile(defaultInfo);

            final SyndFeed feed = new SyndFeedImpl();
            feed.setFeedType(PodCastConstants.FEED_TYPE);

            feed.setTitle(PodCastUtils.getMapValue(defaultFeedValues, PodCastConstants.FEED_TITLE_KEY,
                    PodCastConstants.DEFAULT_FEED_TITLE));
            if (m_feedLink == null)
                feed.setLink(PodCastUtils.generateHttpLink(m_httpRoot, m_fileToCreate, m_urlSuffix));
            else
                feed.setLink(m_feedLink);
            feed.setDescription(PodCastUtils.getMapValue(defaultFeedValues, PodCastConstants.FEED_DESCRIPTION_KEY,
                    PodCastConstants.DEFAULT_FEED_DESCRIPTION));
            feed.setCopyright(PodCastUtils.getMapValue(defaultFeedValues, PodCastConstants.FEED_COPYRIGHT_KEY,
                    PodCastConstants.DEFAULT_FEED_COPYRIGHT));

            //Set the image on the Feed.
            SyndImage img = new SyndImageImpl();
            img.setDescription(feed.getDescription());
            img.setLink(feed.getLink());
            img.setTitle(PodCastUtils.getMapValue(defaultFeedValues, PodCastConstants.FEED_IMAGE_TITLE,
                    PodCastConstants.DEFAULT_IMAGE_TITLE));
            img.setUrl(PodCastUtils.generateHttpLink(m_httpRoot, PodCastUtils.getMapValue(defaultFeedValues,
                    PodCastConstants.FEED_IMAGE_NAME, PodCastConstants.DEFAULT_IMAGE_NAME), m_urlSuffix));
            feed.setImage(img);

            //Set image on all episodes.
            FeedInformation episodeFeed = new FeedInformationImpl();
            episodeFeed.setImage(new URL(img.getUrl()));

            Iterator it = podcastFiles.iterator();
            while (it.hasNext()) {
                SyndEntry podcastEntry = (SyndEntry) it.next();
                podcastEntry.getModules().add(episodeFeed);
            }

            //Generate the itunes image!
            FeedInformation itunesFeed = new FeedInformationImpl();
            itunesFeed.setImage(new URL(img.getUrl()));
            itunesFeed.setSummary(feed.getDescription());
            feed.getModules().add(itunesFeed);

            feed.setEntries(podcastFiles);

            final Writer fileWriter = new FileWriter(m_fileToCreate);
            final SyndFeedOutput feedOutput = new SyndFeedOutput();
            feedOutput.output(feed, fileWriter);
            fileWriter.flush();
            fileWriter.close();

            m_logger.info("Podcast created [" + m_fileToCreate + "]");

        } catch (Exception ex) {
            m_logger.error("ERROR: " + ex.getMessage());
            throw ex;
        }

    }

    private SyndEntry generateEntry(String mp3link, Date fileDate, String fileDescription, String fileCategory,
            String author, PodCastFile originalFile, String httpSuffix) throws PodCastCreationException {
        SyndEntry podcastEntry;
        SyndContent description = new SyndContentImpl();
        SyndCategory category;
        try {

            podcastEntry = new SyndEntryImpl();
            podcastEntry.setAuthor(author);

            podcastEntry.setTitle(originalFile.getTitle());
            podcastEntry.setLink(mp3link + httpSuffix);
            podcastEntry.setPublishedDate(fileDate);

            //Set the right link type
            description.setType(originalFile.getMimeType());

            //Setting description
            m_logger.info("Setting description as [" + fileDescription + "]");

            description.setValue(fileDescription);
            podcastEntry.setDescription(description);

            //Do some crazy category stuff.
            category = new SyndCategoryImpl();
            category.setName(fileCategory);
            List categories = new ArrayList();
            categories.add(category);
            podcastEntry.setCategories(categories);

            ArrayList modules = new ArrayList();
            EntryInformation e = new EntryInformationImpl();
            //Duration in milliseconds... set to 1 minute if not an audio file or cant be found.
            e.setDuration(new Duration(PodCastUtils.getFileDuration(originalFile)));

            e.setAuthor(podcastEntry.getAuthor());
            e.setSummary(podcastEntry.getDescription().getValue());
            e.setKeywords(new String[] { category.getName() });
            modules.add(e);
            podcastEntry.setModules(modules);

            //Set the actual Link.
            SyndEnclosure enc = new SyndEnclosureImpl();
            enc.setType(description.getType());
            enc.setUrl(mp3link + httpSuffix);
            List encs = new ArrayList();
            encs.add(enc);
            podcastEntry.setEnclosures(encs);

            return podcastEntry;

        } catch (Exception ex) {
            m_logger.error("ERROR: " + ex.getMessage());
            throw new PodCastCreationException(ex.getLocalizedMessage());
        }
    }

    private void outputIgnoredFiles() {
        m_logger.info("=========================================================");
        m_logger.info("Ignored Files");

        Iterator it = m_ignoredFiles.keySet().iterator();
        while (it.hasNext()) {
            String file = (String) it.next();
            String reason = (String) m_ignoredFiles.get(file);
            m_logger.info(reason + "\t\t" + file);
        }
        m_logger.info("=========================================================");
    }

}