/**
 * Moxkiriya standalone Wiki.
 * Wiki Repository.
 * 
 * @author Ryuhei Terada
 * See the '<a href="{@docRoot}/copyright.html">Copyright</a>'
 */

package com.wiki.standalone.moxkiriya;
	
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;

import javax.jcr.Credentials;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.NamespaceException;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.Workspace;
import javax.jcr.nodetype.NodeType;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.VersionManager;
import javax.naming.Context;
import javax.naming.InitialContext;

import org.apache.jackrabbit.core.jndi.RegistryHelper;
import org.apache.jackrabbit.value.StringValue;

import com.wiki.standalone.moxkiriya.PageData.FileData;

/**
 * WikiRepository class.
 *
 */
public class WikiRepository {
	/** repository_cluster_derby.xml */
	public static final String REPOSITORY_CUSTER_FILENAME = "repository_cluster_derby.xml";

	/** repository_local.xml */
	public static final String REPOSITORY_LOCAL_FILENAME = "repository_local.xml";	

	public static final String REOPSITORY_DIRNAME = "Repository";
	
	/** Moxkiriya repository generic namespace */
	public static final String NAMESPACE_MOXKI = "moxki";
	
	public static final String NAMESPACE_MOXKI_URI = "https://osdn.net/users/ryuhei__terada/pf/Moxkiriya7/moxki/1.0";

	/** Node name "wikiroot" */
	public static final String NODE_WIKIROOT = NAMESPACE_MOXKI	+ ":wikiroot";

	/** Node name "namespace" */
	public static final String NODE_NAMESPACE = NAMESPACE_MOXKI + ":namespacelist";

	/** Property name "namespace" */
	public static final String PROPERTY_NAMESPACE = NAMESPACE_MOXKI + ":namespace";

	/** Property value "Main" in namespace */
	public static final String PROPERTY_MAIN = "Main";

	/** Property value "Category" in namespace */
	public static final String PROPERTY_CATEGORY = "Category";

	/** Property value "File" in namespace */
	public static final String PROPERTY_FILE = "File";

	/** Node name "pages" */
	public static final String NODE_PAGES = NAMESPACE_MOXKI + ":pages";

	/** Node name "page" */
	public static final String NODE_PAGE = NAMESPACE_MOXKI + ":page";

	/** Property name "title" */
	public static final String PROPERTY_TITLE = NAMESPACE_MOXKI + ":title";

	/** Property name "content" */
	public static final String PROPERTY_CONTENT = NAMESPACE_MOXKI + ":content";

	/** Property name "category" */
	public static final String PROPERTY_category = NAMESPACE_MOXKI + ":category";

	/** Node name "file" */
	public static final String NODE_FILE = NAMESPACE_MOXKI + ":file";
	
	/** Repository Home directory */
	private String repositoryHomeDir_;

	/** repository.xml */
	private String repositoryFile_;

	/** Initial context settings table */
	private static Hashtable<String, String> envHashTable = new  Hashtable<String, String>() {
		private static final long serialVersionUID = 1L;
		{
			put(Context.INITIAL_CONTEXT_FACTORY,
				"org.apache.jackrabbit.core.jndi.provider.DummyInitialContextFactory");
		}
		{
			put(Context.PROVIDER_URL, "localhost");
		}
	};

	/** Repository */
	private Repository repository_;

	/** Session */
	private Session    session_;

	/**
	 * Constructor.
	 * @param wikirootPath
	 * @throws Exception 
	 */
	public WikiRepository(File wikirootPath) throws Exception {
		SettingManager settingMgr = SettingManager.getInstance();

		setSystemProperties();
		
		String configXML = settingMgr.get(SettingManager.SETINGKEY_CLUSTER).equals("true")
				? REPOSITORY_CUSTER_FILENAME
				: REPOSITORY_LOCAL_FILENAME;

		repositoryHomeDir_ = settingMgr.getAbsolutePath(REOPSITORY_DIRNAME);
		repositoryFile_    = settingMgr.getAbsolutePath(configXML);

		InitialContext initialContext = new InitialContext(envHashTable);
    	RegistryHelper.registerRepository(initialContext,
    			"repo",
    			repositoryFile_,
    			repositoryHomeDir_,
    			true);
    	repository_ = (Repository)initialContext.lookup("repo");
    	session_    = repository_.login((Credentials)null);
	}

	/**
	 * Session closer.
	 */
	public void closeSession() {
		session_.logout();
	}
	
	/**
	 * Build Apache Jackrabbit Repository.
	 * @param pageData
	 * @throws Exception 
	 */
	public void buildWikiRepository(PageData pageData) throws Exception {		
		Workspace         workspace  = session_.getWorkspace();

		try {
			NamespaceRegistry nsRegistry = workspace.getNamespaceRegistry();

			nsRegistry.getPrefix(NAMESPACE_MOXKI_URI);
		} catch(NamespaceException e) {
			/*
			 * Build Apache Jackrabbit Repository if namespace"moxki" hasnot registered.
			 */
			workspace.getNamespaceRegistry().registerNamespace(NAMESPACE_MOXKI, NAMESPACE_MOXKI_URI);

	        Node root      = session_.getRootNode();
	        Node wikiroot  = root.addNode(NODE_WIKIROOT);
	        Node namespace = wikiroot.addNode(NODE_NAMESPACE);
	        namespace.setProperty(PROPERTY_NAMESPACE,
	        						new Value[] {
	        							new StringValue(PROPERTY_MAIN), 
	        							new StringValue(PROPERTY_CATEGORY),
	        							new StringValue(PROPERTY_FILE)
	        						});
	        wikiroot.addNode(NODE_PAGES);

	        /*
	         * "page"ノードを作成
	         */
	        checkin(pageData);
		}
	}

	/**
	 * Add pageNode.
	 * @param pageData
	 * @throws Exception 
	 */
	public Node addPageNode(PageData pageData) throws Exception {
    	Node root      = session_.getRootNode();
    	Node wikiroot  = root.getNode(NODE_WIKIROOT);
    	Node pagesNode = wikiroot.getNode(NODE_PAGES);
    	Node pageNode  = pagesNode.addNode(NODE_PAGE);

    	pageNode.addMixin(NodeType.MIX_VERSIONABLE);
    	pageNode.addMixin(NodeType.MIX_REFERENCEABLE);

    	return transformPageDataToNode(pageData, pageNode);
	}

	/**
	 * Transform PageData to node.
	 * @param pageData
	 * @param node
	 * @return Node
	 * @throws Exception
	 */
	public Node transformPageDataToNode(PageData pageData, Node node) throws Exception {
		node.setProperty(PROPERTY_NAMESPACE, new StringValue(pageData.getNamespace()));
		node.setProperty(PROPERTY_TITLE, new StringValue(pageData.getTitle()));
		node.setProperty(PROPERTY_CONTENT, new StringValue(pageData.getContent()));

        ArrayList<String> categories = pageData.getCategories();
    	Value[] values = new Value[] { new StringValue("") };
        if(categories.size() > 0) {
        	values = new Value[categories.size()];
	        for(int count = 0; count < categories.size(); count++) {
	        	values[count] = new StringValue(categories.get(count));
	        }
        }
    	node.setProperty(PROPERTY_category, values);

		FileData fileData = pageData.getFileData();

		if(fileData != null) {
			Node fileNode;			
			if(node.hasNode(WikiRepository.NODE_FILE) == true) {
				fileNode = node.getNode(WikiRepository.NODE_FILE);
			}
			else {
				fileNode     = node.addNode(WikiRepository.NODE_FILE, NodeType.NT_FILE);
				fileNode.addNode(Property.JCR_CONTENT, NodeType.NT_RESOURCE);
			}

			Node         nodeResource = fileNode.getNode(Property.JCR_CONTENT);
			ValueFactory valueFactory = session_.getValueFactory();
			
			nodeResource.setProperty(Property.JCR_DATA, valueFactory.createBinary(fileData.getInputStream()));
			nodeResource.setProperty(Property.JCR_MIMETYPE, new StringValue(fileData.getMimeType()));
			nodeResource.setProperty(Property.JCR_LAST_MODIFIED, valueFactory.createValue(fileData.getLastModified()));
			if(fileData.getMimeType().startsWith("text/") == true) {
				nodeResource.setProperty(Property.JCR_ENCODING, new StringValue("UTF-8"));
			}
		}
    	
        return node;
	}

	/**
	 * Execute query.
	 * @param sql
	 * @return HashMap<String, PageData>
	 * @throws Exception
	 */
	private HashMap<String, PageData> executeQuery(String sql) throws Exception {
		Workspace    workspace = session_.getWorkspace();
		QueryManager queryMgr  = workspace.getQueryManager();
		Query        query     = queryMgr.createQuery(sql, Query.JCR_SQL2);		
        NodeIterator iter      = query.execute().getNodes();

        return convertNodeIteratorToPageDataMap(iter);
	}
	
	/**
	 * Get node matched namespace.
	 * @param pageTitle
	 * @return HashMap<String, PageData>
	 * @throws Exception 
	 */
	public HashMap<String, PageData> queryPageNamespace(String namespace) throws Exception {
        return executeQuery(
				"SELECT * "
				+ "	FROM [nt:unstructured]"
				+ " WHERE"
				+ " ISDESCENDANTNODE(["
					+ "/" + NODE_WIKIROOT + "/" + NODE_PAGES + "])"
				+ " AND"
				+ " [" + PROPERTY_NAMESPACE + "] = '" + namespace + "'"
    		);
	}

	/**
	 * Get node matched uuid.
	 * @param uuid
	 * @return HashMap<String, PageData>
	 * @throws Exception 
	 */
	public HashMap<String, PageData> queryPageUUID(String uuid) throws Exception {
        return executeQuery(
        		"SELECT * " 
        		+ "	FROM [nt:unstructured]"
        		+ " WHERE"
    			+ " ISDESCENDANTNODE(["
					+ "/" + NODE_WIKIROOT + "/" + NODE_PAGES + "])"
        		+ " AND"
        		+ " [" + Property.JCR_UUID + "] = '" + uuid + "'"
    		);
	}
	
	/**
	 * Get node matched page title.
	 * @param pageTitle
	 * @return HashMap<String, PageData>
	 * @throws Exception 
	 */
	public HashMap<String, PageData> queryPageTitle(String pageTitle) throws Exception {
		String       namespace = PROPERTY_MAIN;
		String       title     = pageTitle;
		
		if(pageTitle.contains(":") == true) {
			namespace = pageTitle.substring(0, pageTitle.indexOf(":"));
			title     = pageTitle.substring(pageTitle.indexOf(":") + ":".length());
		}
		
        return queryPageTitle(title, namespace);
	}
	
	/**
	 * Get node matched page title.
	 * @param pageTitle
	 * @param namespace
	 * @return HashMap<String, PageData>
	 * @throws Exception 
	 */
	public HashMap<String, PageData> queryPageTitle(String pageTitle, String namespace) throws Exception {
        return executeQuery(
        		"SELECT * " 
        		+ "	FROM [nt:unstructured]"
        		+ " WHERE"
    			+ " ISDESCENDANTNODE(["
    				+ "/" + NODE_WIKIROOT + "/" + NODE_PAGES + "])"
        		+ " AND"
        		+ " [" + PROPERTY_NAMESPACE + "] = '" + namespace + "'"
        		+ " AND"
        		+ " [" + PROPERTY_TITLE + "] = '" + pageTitle + "'"
			);
	}

	/**
	 * Execute full text search.
	 * @param searchKey
	 * @return String
	 * @throws Exception 
	 */
	public HashMap<String, PageData> queryPageFullTextSearch(String searchKey) throws Exception {
        return executeQuery(
        		"SELECT * " 
        		+ "	FROM [nt:unstructured]"
        		+ " WHERE"
    			+ " ISDESCENDANTNODE(["
    				+ "/" + NODE_WIKIROOT + "/" + NODE_PAGES + "])"
    			+ " AND "
        		+ " CONTAINS([" + PROPERTY_TITLE + "], '" + searchKey + "')"
        		+ " OR "
        		+ " CONTAINS([" + PROPERTY_CONTENT + "], '" + searchKey +  "')"
        	);
	}

	/**
	 * Converter NodeIterator to PageDatMap.
	 * @param nodeIter
	 * @return HashMap<String, PageData>
	 * @throws Exception 
	 */
	public HashMap<String, PageData> convertNodeIteratorToPageDataMap(NodeIterator nodeIter) throws Exception {
        HashMap<String, PageData> pageDataMap = new HashMap<String, PageData>();
        while(nodeIter.hasNext() == true) {
        	Node     node     = nodeIter.nextNode();
        	Version  baseVersion = getBaseVersion(node);
        	PageData pageData = new PageData(node);

        	pageData.setBaseVersion(baseVersion);
        	pageDataMap.put(node.getProperty(Property.JCR_UUID).getString(), pageData);
        }        
		
        return pageDataMap;
	}

	/**
	 * NamespaceList getter
	 * @return Value[]
	 * @throws Exception
	 */
	public Value[] getNamespaceList() throws Exception {
		Node namespace      = session_.getNode("/" + NODE_WIKIROOT + "/" + NODE_NAMESPACE);
		
		return namespace.getProperty(PROPERTY_NAMESPACE).getValues();
	}
	
	/**
	 * Checkout
	 * @param pageData
	 * @throws Exception
	 */
	public void checkout(PageData pageData) throws Exception {
		Node node = pageData.getNode();
		if(node != null) {
			session_.refresh(false);
			
			Workspace      workspace = session_.getWorkspace();
			VersionManager versionMgr = workspace.getVersionManager();
			versionMgr.checkout(node.getPath());
		}
	}

	/**
	 * Checkin.
	 * @param pageData
	 * @throws Exception
	 */
	public PageData checkin(PageData pageData) throws Exception {
		Workspace      workspace  = session_.getWorkspace();
		VersionManager versionMgr = workspace.getVersionManager();
		Node           node       = pageData.getNode();

		if(node == null) {
			node = addPageNode(pageData);
			session_.save();			
			versionMgr.checkout(node.getPath());
		}
		else {
			String uuid = node.getProperty(Property.JCR_UUID).getString();

			node = transformPageDataToNode(pageData, session_.getNodeByIdentifier(uuid));
			session_.save();
		}
		
		versionMgr.checkin(node.getPath());
		createCategoryPages(node);
		
		String uuid = node.getProperty(Property.JCR_UUID).getString();
		
		return new PageData(session_.getNodeByIdentifier(uuid));
	}

	/**
	 * Cancel checkout
	 * @param pageData
	 * @throws Exception
	 */
	public void cancelCheckout(PageData pageData) throws Exception {
		Node node = pageData.getNode();
		if(node != null) {
			Workspace      workspace  = session_.getWorkspace();
			VersionManager versionMgr = workspace.getVersionManager();
			String         path       = node.getPath();
			if(versionMgr.isCheckedOut(path) == true) {
				versionMgr.restore(versionMgr.getBaseVersion(path), true);
			}
		}
	}

	/**
	 * Delete page node.
	 * @param pageData
	 * @throws Exception
	 */
	public void deletePage(PageData pageData) throws Exception {
		pageData.getNode().remove();
		session_.save();			
	}

	/**
	 * Refresh session.
	 * @throws RepositoryException 
	 */
	public void refreshSession() throws RepositoryException {
		session_.refresh(false);
	}
	
	/**
	 * Set SystemProperties. 
	 * @throws Exception
	 */
	private void setSystemProperties() throws Exception {
		SettingManager settingMgr = SettingManager.getInstance();

		String macAddress = getMacAddress();
		String username   = System.getProperty("user.name");
		String approot    = settingMgr.get(SettingManager.SETTINGSKEY_WIKIROOT);
		String partyName  = settingMgr.getSelectedParty();

		System.setProperty("cluster.id", String.valueOf(macAddress.hashCode() 
				+ "-" + partyName
				+ "-" + username));
		System.setProperty("wiki.root", approot);

		if(settingMgr.get(SettingManager.SETINGKEY_CLUSTER).equals("true")) {
			System.setProperty("party.name", partyName);
			System.setProperty("cluster.setting", "TRUE");
			System.setProperty("dbserver.url", settingMgr.get(SettingManager.SETINGKEY_DBSERVER_URL));
			System.setProperty("dbserver.port", settingMgr.get(SettingManager.SETINGKEY_DBSERVER_PORT));
			System.setProperty("jdbc.driver", settingMgr.get(SettingManager.SETINGKEY_JDBC_DRIVER));
		}
		else {
			System.setProperty("cluster.setting", "FALSE");
			System.setProperty("dbserver.url", "");
			System.setProperty("dbserver.port", "");			
			System.setProperty("jdbc.driver", "");
		}
	}

	/**
	 * Mac Address getter.
	 * @return
	 * @throws Exception
	 */
	private String getMacAddress() throws Exception {
		StringBuffer                  buf     = new StringBuffer();
		Enumeration<NetworkInterface> nicList = NetworkInterface.getNetworkInterfaces();
	
		while(nicList.hasMoreElements() == true) {
			NetworkInterface nic = nicList.nextElement();
			if(nic.getName().equals("lo") != true) {
				byte[] macAddress = nic.getHardwareAddress();
				if(macAddress != null) {
					for(byte b: macAddress) {
						buf.append(String.format("%02X-", b));
					}
					break;
				}
			}
		}
		
		return buf.toString();
	}

	/**
	 * 
	 * @param node
	 * @throws Exception 
	 */
	private void createCategoryPages(Node node) throws Exception {
		Workspace      workspace  = session_.getWorkspace();
		VersionManager versionMgr = workspace.getVersionManager();
		Value[]        categories = node.getProperty(PROPERTY_category).getValues();

		for(Value category: categories) {
			String                    title = category.getString();

			if(title.isEmpty() != true) {
				HashMap<String, PageData> map   = queryPageTitle(title, PROPERTY_CATEGORY);
				
				if(map.size() == 0) {
					/*
					 * category pageが未作成の場合、
					 */
					PageData pageData = new PageData();
					pageData.setNamespace(PROPERTY_CATEGORY);
					pageData.setTitle(title);
	
					Node categoryNode = addPageNode(pageData);
					session_.save();
					versionMgr.checkout(categoryNode.getPath());
					versionMgr.checkin(categoryNode.getPath());				
				}
			}
		}
	}

	/**
	 * Test namespace is contains namespaceList.
	 * @param namespace
	 * @return boolean
	 * @throws Exception
	 */
	public boolean isContainsNamespaceList(String namespace) throws Exception {
        Node root          = session_.getRootNode();
        Node wikiroot      = root.getNode(NODE_WIKIROOT);
        Node namespaceNode = wikiroot.getNode(NODE_NAMESPACE);

        Value[] list       = namespaceNode.getProperty(PROPERTY_NAMESPACE).getValues();
        boolean isContains = false;

        for(Value entry: list) {
        	if(entry.getString().equals(namespace) == true) {
        		isContains = true;
        		break;
        	}
        }
        
        return isContains;
	}

	/**
	 * Version history getter.
	 * @param uuid
	 * @return VersionIterator
	 * @throws Exception
	 */
	public VersionIterator getVersionHistory(String uuid) throws Exception {
		HashMap<String, PageData> nodeMap  =  queryPageUUID(uuid);
		PageData                  pageData = nodeMap.values().iterator().next();
		Node                      node     = pageData.getNode();

		Workspace      workspace      = session_.getWorkspace();
		VersionManager versionMgr     = workspace.getVersionManager();
		VersionHistory versionHistory = versionMgr.getVersionHistory(node.getPath());
		
		return versionHistory.getAllVersions();
	}

	/**
	 * Base version getter.
	 * @param node
	 * @return Version
	 * @throws Exception
	 */
	public Version getBaseVersion(Node node) throws Exception {
		Workspace      workspace      = session_.getWorkspace();
		VersionManager versionMgr     = workspace.getVersionManager();
		
		return versionMgr.getBaseVersion(node.getPath());
	}
	
	/**
	 * Restore version.
	 * @param version
	 * @param node
	 * @throws Exception
	 */
	public void restoreVersion(Version version, Node node) throws Exception {
		Workspace      workspace      = session_.getWorkspace();
		VersionManager versionMgr     = workspace.getVersionManager();

		versionMgr.restore(version, true);
	}
	
	/**
	 * Import system view.
	 * @param outputFile
	 * @throws Exception
	 */
	public void importSystemView(File outputFile) throws Exception {
		FileInputStream inStream = new FileInputStream(outputFile);

		try {
	    	Node root      = session_.getRootNode();
	    	Node wikiroot  = root.getNode(NODE_WIKIROOT);
	    	Node pagesNode = wikiroot.getNode(NODE_PAGES);
	
	    	session_.importXML(pagesNode.getPath(), inStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING);
	    	session_.save();

	        Node    namespaceNode = wikiroot.getNode(NODE_NAMESPACE);
	        Value[] namespaces    = namespaceNode.getProperty(PROPERTY_NAMESPACE).getValues();
	    	
	        for(Value namespace: namespaces) {
		    	HashMap<String,PageData> map = queryPageNamespace(namespace.getString());
		    	Iterator<PageData>       iter = map.values().iterator();
		    	while(iter.hasNext() == true) {
		    		PageData pageData = iter.next();
		    		checkout(pageData);
		    		checkin(pageData);
		    	}
	        }
		} finally {
			inStream.close();
		}
	}

	/**
	 * Export system view.
	 * @param outputFile
	 * @throws Exception
	 */
	public void exportSystemView(File outputFile) throws Exception {
		FileOutputStream outStream = new FileOutputStream(outputFile);

		try {
	    	Node root      = session_.getRootNode();
	    	Node wikiroot  = root.getNode(NODE_WIKIROOT);
	    	Node pagesNode = wikiroot.getNode(NODE_PAGES);
	
	    	session_.exportSystemView(pagesNode.getPath(), outStream, false, false);
		} finally {
			outStream.close();
		}
	}
}
