/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. All rights reserved.
 * http://www.jaspersoft.com.
 *
 * Unless you have purchased  a commercial license agreement from Jaspersoft,
 * the following license terms  apply:
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package com.jaspersoft.jasperserver.export.modules.repository;

import com.jaspersoft.jasperserver.api.JSException;
import com.jaspersoft.jasperserver.api.JSExceptionWrapper;
import com.jaspersoft.jasperserver.api.metadata.common.domain.Folder;
import com.jaspersoft.jasperserver.api.metadata.common.domain.InternalURI;
import com.jaspersoft.jasperserver.api.metadata.common.domain.Resource;
import com.jaspersoft.jasperserver.api.metadata.common.domain.ResourceReference;
import com.jaspersoft.jasperserver.api.metadata.common.domain.client.FolderImpl;
import com.jaspersoft.jasperserver.api.metadata.common.domain.util.DataContainerStreamUtil;
import com.jaspersoft.jasperserver.api.metadata.common.service.RepositoryService;
import com.jaspersoft.jasperserver.api.metadata.user.domain.ObjectPermission;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Role;
import com.jaspersoft.jasperserver.api.metadata.user.domain.Tenant;
import com.jaspersoft.jasperserver.api.metadata.user.domain.User;
import com.jaspersoft.jasperserver.api.metadata.user.service.ObjectPermissionService;
import com.jaspersoft.jasperserver.api.metadata.user.service.TenantService;
import com.jaspersoft.jasperserver.export.modules.BaseImporterModule;
import com.jaspersoft.jasperserver.export.modules.ImporterModuleContext;
import com.jaspersoft.jasperserver.export.modules.common.TenantQualifiedName;
import com.jaspersoft.jasperserver.export.modules.repository.beans.FolderBean;
import com.jaspersoft.jasperserver.export.modules.repository.beans.PermissionRecipient;
import com.jaspersoft.jasperserver.export.modules.repository.beans.RepositoryObjectPermissionBean;
import com.jaspersoft.jasperserver.export.modules.repository.beans.ResourceBean;
import com.jaspersoft.jasperserver.export.modules.repository.beans.ResourceReferenceBean;
import com.jaspersoft.jasperserver.export.util.PathUtils;
import com.jaspersoft.jasperserver.export.util.PathUtils.SplittedPath;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Element;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 * @version $Id: ResourceImporter.java 20126 2011-02-06 13:13:44Z vsabadosh $
 */
public class ResourceImporter extends BaseImporterModule implements ResourceImportHandler, InitializingBean {
	
	public final static String ATTRIBUTE_UPDATE_RESOURCES = "updateResources";
	
	private final static Log log = LogFactory.getLog(ResourceImporter.class);
	
	private ResourceModuleConfiguration configuration;
    private TenantService tenantService;
	private String prependPathArg;
	private String updateArg;
    private List<String> rootSubTenantFolderUris;
    private Pattern orgPattern = Pattern.compile("((/" + TenantService.ORGANIZATIONS + "/[^/]+)*)");

	protected RepositoryService repository;
	protected String prependPath;
	private boolean update;

	protected Set importedURIs;
	private LinkedList folderQueue;
	private LinkedList resourceQueue;
	private Map roles;
	private Map users;

	public void afterPropertiesSet() {
		this.repository = configuration.getRepository();
	}

	public void init(ImporterModuleContext moduleContext) {
		super.init(moduleContext);
		configuration.setApplicationContext((ApplicationContext) moduleContext.getAttributes().getAttribute("appContext"));
		prependPath = getPrependPath();
		update = getUpdateFlag();
        //Retrieve all root subtenants
        List<Tenant> allRootSubTenantList = tenantService.getAllSubTenantList(executionContext, TenantService.ORGANIZATIONS);
        rootSubTenantFolderUris = new ArrayList<String>();
        for (Tenant tenant : allRootSubTenantList) {
            rootSubTenantFolderUris.add(tenant.getTenantFolderUri());
        }
	}

	protected String getPrependPath() {
		String path = getParameterValue(getPrependPathArg());
		if (path != null) {
			path = PathUtils.normalizePath(path);
			if (path.length() == 0 || path.equals(Folder.SEPARATOR)) {
				path = null;
			} else if (!path.startsWith(Folder.SEPARATOR)) {
				path = Folder.SEPARATOR + path;
			}
		}
		return path;
	}

	protected boolean getUpdateFlag() {
		return hasParameter(getUpdateArg());
	}

	protected boolean isUpdate() {
		return update;
	}
	
	public void process() {
		initProcess();
		
		createPrependFolder();
		
		queueEntryFolders();
		queueEntryResources();
		
		while (!folderQueue.isEmpty() || !resourceQueue.isEmpty()) {
			if (folderQueue.isEmpty()) {
				String uri = (String) resourceQueue.removeFirst();
				try {
                    importResource(uri, false);
                } catch (ResourceBeanDataNotFoundException e) {
                    final String msg;
                    if (uri.equals(e.getMessage())) {
                        msg = "Reference resource \"" + e.getMessage() +
                                "\" not found.";
                    } else {
                        msg = "Reference resource \"" + e.getMessage() +
                                "\" not found when importing resource \"" + uri + "\".";
                    }
                    commandOut.warn(msg);
                    fileBrokenResource(msg);
                }
			} else {
				String uri = (String) folderQueue.removeFirst();
				importFolder(uri, true);
			}
		}
	}

    private void fileBrokenResource(String message) {
        File file = new File("import-errors.log");
        Writer out = null;
        try {
            out = new BufferedWriter(new FileWriter(file, true));
            out.write(message);
            out.write('\n');
        } catch (IOException e) {
            commandOut.error("Error writing to import.log", e);
        }
        finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e1) {}
            }
        }
    }

    protected void queueEntryFolders() {
		List entryFolders = new ArrayList();
		for (Iterator it = indexElement.elementIterator(configuration.getFolderIndexElement()); it.hasNext(); ) {
			Element folderElement = (Element) it.next();
			entryFolders.add(folderElement.getText());
		}

		if (!entryFolders.isEmpty()) {
			Collections.sort(entryFolders);
			for (Iterator it = entryFolders.iterator(); it.hasNext();) {
				String uri = (String) it.next();
				folderQueue.addLast(uri);
			}
		}
	}

	protected void queueEntryResources() {
		for (Iterator it = indexElement.elementIterator(configuration.getResourceIndexElement()); it.hasNext(); ) {
			Element resourceElement = (Element) it.next();
			String uri = resourceElement.getText();
			resourceQueue.addLast(uri);
		}
	}

	protected void createPrependFolder() {
		if (prependPath != null) {
			LinkedList toCreateURIs = new LinkedList();
			for (String path = prependPath; 
					repository.getFolder(executionContext, path) == null; 
					path = PathUtils.splitPath(path).parentPath) {
				toCreateURIs.addFirst(path);
			}
			
			while(!toCreateURIs.isEmpty()) {
				String path = (String) toCreateURIs.removeFirst();
				Folder folder = createFolder(path);
				
				commandOut.debug("About to save folder " + path);
				
				repository.saveFolder(executionContext, folder);
			}
		}
	}

	protected void initProcess() {
		importedURIs = new HashSet();
		folderQueue = new LinkedList();
		resourceQueue = new LinkedList();
		
		roles = new HashMap();
		users = new HashMap();
	}

	protected void importFolder(String uri, boolean detailsRequired) {
		if (importedURIs.contains(uri)) {
			return;
		}

        // All organization folder uris have pattern /organizations/org_1../organizations/org2/folder except of template folder
        // which has pattern /organizations/org_template, so we have to skip it.
        String tenantFolderUri = "";
        int orgTeplateIndex = uri.indexOf("/" + TenantService.ORGANIZATIONS + "/" + TenantService.ORG_TEMPLATE);
        if (orgTeplateIndex >= 0) {
            tenantFolderUri = uri.substring(0, orgTeplateIndex);
        } else {
            Matcher m = orgPattern.matcher(uri);
            if (m.find()) {
                tenantFolderUri = m.group(1);
            }
        }

        if (!(rootSubTenantFolderUris.contains(tenantFolderUri) || tenantFolderUri.equals(""))) {
            commandOut.info("Folder with the uri" + uri+" is attached to not existing organization. Not imported");
            return;
        }

		FolderBean folderBean = getFolderDetails(uri, detailsRequired);
		
		String importUri = prependedPath(uri);
		Folder folder = repository.getFolder(executionContext, importUri);
		if (folder == null) {
			ensureParent(uri);

			if (folderBean == null) {
				folder = createFolder(importUri);
			} else {
				folder = createFolder(folderBean);
			}

            if (executionContext.getAttributes() == null) {
                executionContext.setAttributes(new ArrayList());
            }
            executionContext.getAttributes().add(RepositoryService.IS_IMPORTING);
            commandOut.debug("About to save folder " + importUri);
            repository.saveFolder(executionContext, folder);
			
			if (folderBean != null) {
				setPermissions(folder, folderBean.getPermissions(), false);
			}
			
			commandOut.info("Created repository folder " + importUri);
		} else if (update && folderBean != null) {
            folder.setLabel(folderBean.getLabel());
            folder.setDescription(folderBean.getDescription());
            
            commandOut.debug("About to save folder " + importUri);
            repository.saveFolder(executionContext, folder);
            commandOut.info("Updating folder " + importUri);

            ObjectPermissionService permissionsService = configuration.getPermissionService();
            permissionsService.deleteObjectPermissionForObject(executionContext, folder);

    		setPermissions(folder, folderBean.getPermissions(), false);
        } else {
			if (folderBean != null) {
                commandOut.info("Folder " + importUri + " already exists, importing permissions only");
				
				setPermissions(folder, folderBean.getPermissions(), true);
			}
		}
		
		importedURIs.add(uri);
		
		if (folderBean != null) {
			queueSubFolders(uri, folderBean);
			
			queueResources(uri, folderBean);
		}
	}

	protected void queueSubFolders(String uri, FolderBean folderBean) {
		String[] subFolders = folderBean.getSubFolders();
		if (subFolders != null) {
			for (int i = 0; i < subFolders.length; i++) {
				String subfolderURI = appendPath(uri, subFolders[i]);
				folderQueue.addLast(subfolderURI);
			}
		}
	}

	protected void queueResources(String uri, FolderBean folderBean) {
		String[] resources = folderBean.getResources();
		if (resources != null) {
			for (int i = 0; i < resources.length; i++) {
				String resourceUri = appendPath(uri, resources[i]);
				resourceQueue.addLast(resourceUri);
			}
		}
	}

	protected String prependedPath(String uri) {
		return PathUtils.concatPaths(prependPath, uri);
	}

	protected String appendPath(String uri, String name) {
		String subUri;
		if (uri.equals(Folder.SEPARATOR)) {
			subUri = Folder.SEPARATOR + name;
		} else {
			subUri = uri + Folder.SEPARATOR + name;
		}
		return subUri;
	}

	protected FolderBean getFolderDetails(String uri, boolean required) {
		FolderBean folderBean = null;
		String folderPath = PathUtils.concatPaths(configuration.getResourcesDirName(), uri);
		if (input.fileExists(folderPath, configuration.getFolderDetailsFileName())) {
			folderBean = (FolderBean) deserialize(folderPath, configuration.getFolderDetailsFileName(), configuration.getSerializer());
		} else {
			if (required) {
                // Adding non localized message cause import-export tool does not support localization.
                StringBuilder message = new StringBuilder("Folder details for folder ");
                message.append(uri);
                message.append(" were not found in the import information.");
                commandOut.info(message.toString());
				throw new JSException(message.toString());       
			}
 		}
		return folderBean;
	}
	
	protected void ensureParent(String uri) {
		SplittedPath splitPath = PathUtils.splitPath(uri);
		if (splitPath != null && splitPath.parentPath != null) {
			importFolder(splitPath.parentPath, false);
		}
	}

	protected Folder createFolder(String uri) {
		Folder folder = new FolderImpl();
		SplittedPath splPath = PathUtils.splitPath(uri);
		folder.setParentFolder(splPath.parentPath);
		folder.setName(splPath.name);
		folder.setLabel(splPath.name);
		return folder;
	}
	
	protected Folder createFolder(FolderBean folderBean) {
		Folder folder = new FolderImpl();
		folderBean.copyTo(folder);
		folder.setCreationDate(folderBean.getCreationDate());
		folder.setUpdateDate(folderBean.getUpdateDate());
		folder.setParentFolder(prependedPath(folder.getParentFolder()));
		return folder;
	}

	protected String importResource(String uri, boolean ignoreMissing) {
		String importUri = prependedPath(uri);
		if (!importedURIs.contains(uri)) {
			if (ignoreMissing && !hasResourceBeanData(uri)) {
				commandOut.info("Resource " + uri + " data missing from the catalog, skipping from import");
			} else {
                if (!hasResourceBeanData(uri)) {
                    throw new ResourceBeanDataNotFoundException(uri);
                }

				Resource resource = repository.getResource(executionContext, importUri);
				if (resource == null) {
					ensureParent(uri);
					
					ResourceBean bean = readResourceBean(uri);
					resource = createResource(bean);

                    if (executionContext.getAttributes() == null) {
                        executionContext.setAttributes(new ArrayList());
                    }
                    executionContext.getAttributes().add(RepositoryService.IS_IMPORTING);
                    commandOut.debug("About to save resource " + importUri);
                    repository.saveResource(executionContext, resource);
					
					setPermissions(resource, bean.getPermissions(), false);
					
					commandOut.info("Imported resource " + importUri);
				} else if (update) {
					registerUpdateResource(importUri);
					
					ResourceBean bean = readResourceBean(uri);
					Resource updated = createResource(bean);
					
					if (resource.isSameType(updated)) {
						updated.setVersion(resource.getVersion());
                        commandOut.debug("About to save resource " + importUri);
                        repository.saveResource(executionContext, updated);
						commandOut.info("Updated resource " + importUri);
					} else {
						commandOut.warn("Resource " + importUri 
								+ " already exists in the repository and has a different type than in the catalog, not updating");
					}
				} else {
					commandOut.warn("Resource " + importUri + " already exists, not importing");
				}
				
				importedURIs.add(uri);
			}
		}
		return importUri;
	}

	protected void registerUpdateResource(String resourceUri) {
		Set updateResources = (Set) getContextAttributes().getAttribute(
				ATTRIBUTE_UPDATE_RESOURCES);
		if (updateResources == null) {
			updateResources = new HashSet();
			getContextAttributes().setAttribute(ATTRIBUTE_UPDATE_RESOURCES, 
					updateResources);
		}
		
		updateResources.add(resourceUri);
	}

	protected Resource createResource(ResourceBean bean) {
		Class resourceItf = configuration.getCastorBeanMappings().getInterface(bean.getClass());
		Resource resource = repository.newResource(executionContext, resourceItf);
		bean.copyTo(resource, this);
		resource.setParentFolder(prependedPath(resource.getParentFolder()));
		return resource;
	}

	protected boolean hasResourceBeanData(String uri) {
		String resourceFileName = getResourceFileName(uri);
		return input.fileExists(configuration.getResourcesDirName(), resourceFileName);
	}
	
	protected ResourceBean readResourceBean(String uri) {
		String resourceFileName = getResourceFileName(uri);
		ResourceBean bean = (ResourceBean) deserialize(configuration.getResourcesDirName(), resourceFileName, configuration.getSerializer());
		return bean;
	}
	
	protected String getResourceFileName(String uri) {
		return uri + ".xml";
	}

    @Override
    public String getSourceJsVersion() {
        return super.getSourceJsVersion();
    }

    @Override
    public String getTargetJsVersion() {
        return super.getTargetJsVersion();
    }

    public ResourceReference handleReference(ResourceReferenceBean beanReference) {
		ResourceReference reference;
		if (beanReference == null) {
			reference = null;
		} else if (beanReference.isLocal()) {
			ResourceBean localResBean = beanReference.getLocalResource();
			Resource localRes = createResource(localResBean);
			reference = new ResourceReference(localRes);
		} else {
			String referenceURI = beanReference.getExternalURI();
			if (referenceURI == null || referenceURI.equals("")) {
                return null;
            }
			importResource(referenceURI, false);
			reference = new ResourceReference(prependedPath(referenceURI));
		}
		return reference;
	}

	public byte[] handleData(ResourceBean resourceBean, String dataFile, String providerId) {
		String filename = PathUtils.concatPaths(resourceBean.getFolder(), dataFile);
		InputStream dataInput = getFileInput(configuration.getResourcesDirName(), filename);
		boolean closeInput = true;
		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			DataContainerStreamUtil.pipeData(dataInput, out);
			
			closeInput = false;
			dataInput.close();
			
			return out.toByteArray();
		} catch (IOException e) {
			log.error(e);
			throw new JSExceptionWrapper(e);
		} finally {
			if (closeInput) {
				try {
					dataInput.close();
				} catch (IOException e) {
					log.error(e);
				}
			}
		}
	}

	public Resource handleResource(ResourceBean resource) {
		Resource res = createResource(resource);
		return res;
	}

	public String handleResource(String uri) {
		return handleResource(uri, false);
	}

	public String handleResource(String uri, boolean ignoreMissing) {
		return importResource(uri, ignoreMissing);
	}

	protected void setPermissions(InternalURI object, RepositoryObjectPermissionBean[] permissions, boolean checkExisting) {
		if (permissions != null) {
			for (int i = 0; i < permissions.length; i++) {
				RepositoryObjectPermissionBean permissionBean = permissions[i];
				setPermission(object, permissionBean, checkExisting);
			}
		}
	}

	protected void setPermission(InternalURI object, RepositoryObjectPermissionBean permissionBean, boolean checkExisting) {
		ObjectPermissionService permissionsService = configuration.getPermissionService();
		
		PermissionRecipient permissionRecipient = permissionBean.getRecipient();
		Object recipient;
		String recipientType = permissionRecipient.getRecipientType();
		if (recipientType.equals(configuration.getPermissionRecipientRole())) {
			recipient = getRole(permissionRecipient);
			if (recipient == null) {
				commandOut.warn("Role " + permissionRecipient + " not found, skipping permission of " + object.getURI());
			}
		} else if (recipientType.equals(configuration.getPermissionRecipientUser())) {
			recipient = getUser(permissionRecipient);
			if (recipient == null) {
				commandOut.warn("User " + permissionRecipient + " not found, skipping permission of " + object.getURI());
			}
		} else {
			recipient = null;
			commandOut.warn("Unknown object permission recipient type " + recipientType + ", skipping permission of " + object.getURI());
		}
		
		if (recipient != null) {
			boolean existing;
			if (checkExisting) {
				List permissions = permissionsService.getObjectPermissionsForObjectAndRecipient(executionContext, object, recipient);
				existing = permissions != null && !permissions.isEmpty();
			} else {
				existing = false;
			}            
			if (existing) {
				if (log.isInfoEnabled()) {
					log.info("Permission on " + object.getURI() + " for " + permissionRecipient + " already exists, skipping.");
				}
			} else {
				ObjectPermission permission = permissionsService.newObjectPermission(executionContext);
				permission.setURI(object.getURI());
				permission.setPermissionMask(permissionBean.getPermissionMask());
				permission.setPermissionRecipient(recipient);
				
				permissionsService.putObjectPermission(executionContext, permission);
			}
		}		
	}

	protected Role getRole(TenantQualifiedName roleName) {
		Role role;
		if (roles.containsKey(roleName)) {
			role = (Role) roles.get(roleName);
		} else {
			role = loadRole(roleName);
			roles.put(roleName, role);
		}
		return role;
	}

	protected Role loadRole(TenantQualifiedName roleName) {
		return configuration.getAuthorityService().getRole(executionContext, roleName.getName());
	}

	protected User getUser(TenantQualifiedName username) {
		User user;
		if (users.containsKey(username)) {
			user = (User) users.get(username);
		} else {
			user = loadUser(username);
			users.put(username, user);
		}
		return user;
	}

	protected User loadUser(TenantQualifiedName username) {
		return configuration.getAuthorityService().getUser(executionContext, username.getName());
	}

	public ResourceModuleConfiguration getConfiguration() {
		return configuration;
	}

	public void setConfiguration(ResourceModuleConfiguration configuration) {
		this.configuration = configuration;
	}

    public TenantService getTenantService() {
        return tenantService;
    }

    public void setTenantService(TenantService tenantService) {
        this.tenantService = tenantService;
    }

    public String getPrependPathArg() {
		return prependPathArg;
	}

	public void setPrependPathArg(String prependPathArg) {
		this.prependPathArg = prependPathArg;
	}

	public String getUpdateArg() {
		return updateArg;
	}

	public void setUpdateArg(String updateArg) {
		this.updateArg = updateArg;
	}

}
