org.xwiki.job.internal.DefaultJobStatusStore.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.job.internal.DefaultJobStatusStore.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.job.internal;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.collections4.map.LRUMap;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.job.DefaultJobStatus;
import org.xwiki.job.JobManagerConfiguration;
import org.xwiki.job.JobStatusStore;
import org.xwiki.job.annotation.Serializable;
import org.xwiki.job.event.status.JobStatus;

/**
 * Default implementation of {@link JobStatusStorage}.
 *
 * @version $Id: d361ecede6c5045c60f049edb56238f5b156bb49 $
 * @since 6.1M2
 */
@Component
@Singleton
public class DefaultJobStatusStore implements JobStatusStore, Initializable {
    private static class Cache implements Map<List<String>, JobStatus> {
        // TODO: probably use JCache instead
        private final Map<List<String>, JobStatus> cache;

        private final Set<List<String>> noStatusCache;

        Cache(int size) {
            this.cache = new LRUMap<>(size);
            this.noStatusCache = new HashSet<List<String>>();
        }

        @Override
        public int size() {
            return this.cache.size() + this.noStatusCache.size();
        }

        @Override
        public boolean isEmpty() {
            return this.cache.isEmpty() && this.noStatusCache.isEmpty();
        }

        @Override
        public boolean containsKey(Object key) {
            return this.cache.containsKey(key) || this.noStatusCache.contains(key);
        }

        @Override
        public boolean containsValue(Object value) {
            // not used
            return false;
        }

        @Override
        public JobStatus get(Object key) {
            if (!this.noStatusCache.contains(key)) {
                return this.cache.get(key);
            }

            return NOSTATUS;
        }

        @Override
        public JobStatus put(List<String> key, JobStatus value) {
            JobStatus status = get(key);

            if (value == null) {
                this.cache.remove(key);
                this.noStatusCache.add(key);
            } else {
                this.cache.put(key, value);
                this.noStatusCache.remove(key);
            }

            return status;
        }

        @Override
        public JobStatus remove(Object key) {
            JobStatus status = get(key);

            this.cache.remove(key);
            this.noStatusCache.remove(key);

            return status;
        }

        @Override
        public void putAll(Map<? extends List<String>, ? extends JobStatus> m) {
            for (Map.Entry<? extends List<String>, ? extends JobStatus> entry : m.entrySet()) {
                put(entry.getKey(), entry.getValue());
            }
        }

        @Override
        public void clear() {
            this.cache.clear();
            this.noStatusCache.clear();
        }

        @Override
        public Set<List<String>> keySet() {
            // not used
            return null;
        }

        @Override
        public Collection<JobStatus> values() {
            // not used
            return null;
        }

        @Override
        public Set<java.util.Map.Entry<List<String>, JobStatus>> entrySet() {
            // not used
            return null;
        }
    }

    /**
     * The name of the file where the job status is stored.
     */
    private static final String FILENAME_STATUS = "status.xml";

    /**
     * Encoding used for file content and names.
     */
    private static final String DEFAULT_ENCODING = "UTF-8";

    /**
     * The encoded version of a <code>null</code> value in the id list.
     */
    private static final String FOLDER_NULL = "&null";

    private static final JobStatus NOSTATUS = new DefaultJobStatus(null, null, null, null);

    /**
     * Used to get the storage directory.
     */
    @Inject
    private JobManagerConfiguration configuration;

    /**
     * The logger to log.
     */
    @Inject
    private Logger logger;

    private JobStatusSerializer serializer;

    private ExecutorService executorService;

    private Map<List<String>, JobStatus> cache;

    class JobStatusSerializerRunnable implements Runnable {
        /**
         * The status to store.
         */
        private final JobStatus status;

        JobStatusSerializerRunnable(JobStatus status) {
            this.status = status;
        }

        @Override
        public void run() {
            saveJobStatus(this.status);
        }
    }

    @Override
    public void initialize() throws InitializationException {
        try {
            this.serializer = new JobStatusSerializer();

            repair();
        } catch (Exception e) {
            this.logger.error("Failed to load jobs", e);
        }

        BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("Job status serializer")
                .daemon(true).priority(Thread.MIN_PRIORITY).build();
        this.executorService = new ThreadPoolExecutor(0, 10, 60L, TimeUnit.SECONDS,
                new SynchronousQueue<Runnable>(), threadFactory);

        this.cache = Collections.synchronizedMap(new Cache(this.configuration.getJobStatusCacheSize()));
    }

    /**
     * @param name the file or directory name to encode
     * @return the encoding name
     */
    private String encode(String name) {
        String encoded;

        if (name != null) {
            try {
                encoded = URLEncoder.encode(name, DEFAULT_ENCODING);
            } catch (UnsupportedEncodingException e) {
                // Should never happen

                encoded = name;
            }
        } else {
            encoded = FOLDER_NULL;
        }

        return encoded;
    }

    /**
     * Load jobs from directory.
     * 
     * @throws IOException when failing to load statuses
     */
    private void repair() throws IOException {
        File folder = this.configuration.getStorage();

        if (folder.exists()) {
            if (!folder.isDirectory()) {
                throw new IOException("Not a directory: " + folder);
            }

            repairFolder(folder);
        }
    }

    /**
     * @param folder the folder from where to load the jobs
     */
    private void repairFolder(File folder) {
        for (File file : folder.listFiles()) {
            if (file.isDirectory()) {
                repairFolder(file);
            } else if (file.getName().equals(FILENAME_STATUS)) {
                try {
                    JobStatus status = loadStatus(folder);

                    if (status != null) {
                        File properFolder = getJobFolder(status.getRequest().getId());

                        if (!folder.equals(properFolder)) {
                            // Move the status in its right place
                            try {
                                FileUtils.moveFileToDirectory(file, properFolder, true);
                            } catch (IOException e) {
                                this.logger.error("Failed to move job status file", e);
                            }
                        }
                    }
                } catch (Exception e) {
                    this.logger.warn("Failed to load job status in folder [{}]", folder, e);
                }
            }
        }
    }

    private JobStatus loadStatus(List<String> id) {
        return loadStatus(getJobFolder(id));
    }

    /**
     * @param folder the folder from where to load the job status
     */
    private JobStatus loadStatus(File folder) {
        File statusFile = new File(folder, FILENAME_STATUS);
        if (statusFile.exists()) {
            return loadJobStatus(statusFile);
        }

        return null;
    }

    /**
     * @param statusFile the file containing job status to load
     * @return the job status
     * @throws Exception when failing to load the job status from the file
     */
    private JobStatus loadJobStatus(File statusFile) {
        return this.serializer.read(statusFile);
    }

    // JobStatusStorage

    /**
     * @param id the id of the job
     * @return the folder where to store the job related informations
     */
    private File getJobFolder(List<String> id) {
        File folder = this.configuration.getStorage();

        if (id != null) {
            for (String idElement : id) {
                folder = new File(folder, encode(idElement));
            }
        }

        return folder;
    }

    /**
     * @param status the job status to save
     * @throws IOException when falling to store the provided status
     */
    private void saveJobStatus(JobStatus status) {
        try {
            File statusFile = getJobFolder(status.getRequest().getId());
            statusFile = new File(statusFile, FILENAME_STATUS);

            this.serializer.write(status, statusFile);
        } catch (Exception e) {
            this.logger.warn("Failed to save job status [{}]", status, e);
        }
    }

    @Override
    public JobStatus getJobStatus(List<String> id) {
        JobStatus status = this.cache.get(id);

        if (status == null) {
            synchronized (this.cache) {
                status = this.cache.get(id);

                if (status == null) {
                    try {
                        status = loadStatus(id);

                        this.cache.put(id, status);
                    } catch (Exception e) {
                        this.logger.warn("Failed to load job status for id [{}]", id, e);

                        this.cache.put(id, null);
                    }
                }
            }
        }

        return status == NOSTATUS ? null : status;
    }

    @Override
    public void store(JobStatus status) {
        store(status, false);
    }

    @Override
    public void storeAsync(JobStatus status) {
        store(status, true);
    }

    private void store(JobStatus status, boolean async) {
        if (status != null && status.getRequest() != null && status.getRequest().getId() != null) {
            synchronized (this.cache) {
                this.cache.put(status.getRequest().getId(), status);
            }

            // Only store Serializable job status on file system
            if (status.getClass().isAnnotationPresent(Serializable.class)
                    || status instanceof java.io.Serializable) {
                if (async) {
                    this.executorService.execute(new JobStatusSerializerRunnable(status));
                } else {
                    saveJobStatus(status);
                }
            }
        }
    }

    @Override
    public void remove(List<String> id) {
        File jobFolder = getJobFolder(id);

        if (jobFolder.exists()) {
            try {
                FileUtils.deleteDirectory(jobFolder);
            } catch (IOException e) {
                this.logger.warn("Failed to delete job folder [{}]", jobFolder, e);
            }

            this.cache.remove(id);
        }

        this.cache.remove(id);
    }
}