net.gbmb.collector.RecordController.java Source code

Java tutorial

Introduction

Here is the source code for net.gbmb.collector.RecordController.java

Source

/*
 * Copyright 2014 Guillaume Bailleul
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.gbmb.collector;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hazelcast.core.HazelcastInstance;
import com.hazelcast.core.IMap;
import com.hazelcast.core.MultiMap;
import net.gbmb.collector.dao.Application;
import net.gbmb.collector.dao.ApplicationRepository;
import net.gbmb.collector.dao.CollectionRepository;
import net.gbmb.collector.dao.EndedCollection;
import org.apache.commons.codec.binary.Base64InputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.regex.Pattern;

@RestController
public class RecordController {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    public static final String TEST_KEY = "test";
    public static final int CID_MIN_SIZE = 6;
    public static final int CID_MAX_SIZE = 36;

    @Autowired
    public ApplicationContext context;

    @Resource
    public HazelcastInstance hazelcast;

    @Resource(name = "collection-map")
    public IMap<String, Collection> collectionMap;

    @Resource(name = "collection-records")
    public MultiMap<String, CollectionRecord> collectionRecords;

    @Resource(name = "push-queue")
    private BlockingQueue pushQueue;

    @Resource
    private ApplicationRepository applicationRepository;

    @Resource
    private CollectionRepository collectionRepository;

    @Resource
    private RecordValidator recordValidator;

    @Resource
    private TemporaryStorage temporaryStorage;

    private Pattern cidPattern = Pattern.compile("[0-9a-zA-Z-]{4,36}");

    @RequestMapping(value = "/records/{cid}/create", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    public Collection createNamedCollection(@PathVariable("cid") String cid,
            @RequestParam(required = false, defaultValue = "defapp", value = "app") String appShortName,
            @RequestParam(required = false, defaultValue = "run", value = "mode") String mode)
            throws CollectionStateException {
        // ensure cid is valide
        validateCid(cid);
        // retrieve application configuration
        Application application = applicationRepository.findByShortName(appShortName);
        // if application not existing, cannot continue
        if (application == null) {
            throw new IllegalArgumentException("Application not existing: " + appShortName);
        }
        return registerCollection(application, cid, mode);
    }

    @RequestMapping(value = "/records/create", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
    public Collection createNotNamedCollection(
            @RequestParam(required = false, defaultValue = "defapp", value = "app") String appShortName,
            @RequestParam(required = false, defaultValue = "run", value = "mode") String mode)
            throws CollectionStateException {
        // retrieve application configuration
        Application application = applicationRepository.findByShortName(appShortName);
        // if application not existing, cannot continue
        if (application == null) {
            throw new IllegalArgumentException("Application not existing: " + appShortName);
        }
        // Create cid
        String cid = UUID.randomUUID().toString();
        return registerCollection(application, cid, mode);
    }

    private Collection registerCollection(Application application, String cid, String mode)
            throws CollectionStateException {
        boolean isTest = TEST_KEY.equals(mode);
        Lock lock = hazelcast.getLock(cid);
        lock.lock();
        try {
            if (collectionRepository.findByCollectionId(cid) != null) {
                // collection already existing
                throw new CollectionStateException(String.format("Collection already existing: %s", cid));
            }
            Collection collection = new Collection(cid);
            collection.setCreationDate(new Date());
            collection.setState(CollectionState.COLLECTING);
            collection.setCancelable(application.getCancelableCollection());
            collection.setTestMode(isTest);
            if (collectionMap.putIfAbsent(cid, collection) == null) {
                // no previous record
                return collection;
            } else {
                // already existing
                throw new CollectionStateException("Already existising: " + cid);
            }
        } finally {
            lock.unlock();
        }
    }

    private void validateCid(String cid) {
        if (cid == null) {
            throw new IllegalArgumentException("cid cannot be null");
        } else if (cid.length() < CID_MIN_SIZE) {
            throw new IllegalArgumentException(
                    String.format("Cid is too short, %d is less than %d", cid.length(), CID_MIN_SIZE));
        } else if (cid.length() > CID_MAX_SIZE) {
            throw new IllegalArgumentException(
                    String.format("Cid is too long, %d is greater than %d", cid.length(), CID_MAX_SIZE));
        } else if (!cidPattern.matcher(cid).matches()) {
            throw new IllegalArgumentException(String.format("Invalid character found in cid '%s'", cid));
        }
        // else OK
    }

    @RequestMapping(value = "/records/{cid}/end", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    public void endCollection(final @PathVariable("cid") String cid) throws CollectionStateException {
        Lock lock = hazelcast.getLock(cid);
        lock.lock();
        try {
            Collection collection = collectionMap.get(cid);
            if (collection == null) {
                // not existing collection
                throw new CollectionStateException("Collection not existing");
            } else if (collection.getState() == CollectionState.ENDED) {
                throw new CollectionStateException("Collection already ended");
            }
            // mark collection ended
            markCollectionEnded(collection);
        } finally {
            lock.unlock();
        }
        // post treatment on collection: push
        // done outside of lock
        pushQueue.offer(cid);
    }

    @RequestMapping(value = "/records/{cid}/cancel", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
    public Collection cancelCollection(@PathVariable("cid") String cid) throws CollectionStateException {
        Lock lock = hazelcast.getLock(cid);
        try {
            lock.lock();
            Collection collection = collectionMap.get(cid);
            if (collection == null) {
                // collection not existing
                throw new ResourceNotFoundException(String.format("No collection with id '%s'", cid));
            }
            if (CollectionState.COLLECTING != collection.getState()) {
                // COLLECTING is the unique state that can be canceled
                throw new CollectionStateException(String.format("Cannot cancel collection '%s' in state '%s'",
                        collection.getId(), collection.getState().name()));
            }
            if (collection.isCancelable()) {
                // OK, can cancel
                collectionRecords.remove(cid);
                collectionMap.remove(cid);
                throw new NoContentException(String.format("Collection with id '%s' cancelled", cid));
            } else {
                // not cancelable so end the collection
                return markCollectionEnded(collection);
            }
        } finally {
            lock.unlock();
        }
    }

    private Collection markCollectionEnded(Collection collection) {
        // mark collection ended
        collection.setState(CollectionState.ENDED);
        collectionMap.put(collection.getId(), collection);
        // Store the collection in the repository
        EndedCollection ended = new EndedCollection();
        ended.setState(collection.getState());
        ended.setCollectionId(collection.getId());
        ended.setCreationDate(collection.getCreationDate());
        ended.setEndDate(new Date());
        ended.setCancelable(collection.isCancelable());
        collectionRepository.save(ended);
        return collection;
    }

    @RequestMapping(value = "/records/{cid}/add", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    public void record(@PathVariable("cid") String cid, @RequestBody CollectionRecord record)
            throws CollectionStateException {
        // check if collection exists
        if (!collectionMap.containsKey(cid)) {
            // collection not existing
            throw new CollectionStateException("Collection not existing");
        }
        // check if record is valid
        if (!recordValidator.validate(record)) {
            // record not valid
            throw new IllegalArgumentException("Record is invalid");
        }
        addRecord(cid, record);
    }

    private void addRecord(String cid, CollectionRecord record) throws CollectionStateException {
        Lock lock = hazelcast.getLock(cid);
        lock.lock();
        try {
            Collection collection = collectionMap.get(cid);
            if (collection == null) {
                throw new CollectionStateException("Collection not existing");
            } else if (collection.getState() != CollectionState.COLLECTING) {
                throw new CollectionStateException(collection.getState(), "Collection not in collecting state");
            } else {
                collectionRecords.put(cid, record);
            }
        } finally {
            lock.unlock();
        }
    }

    @RequestMapping(value = "/records/{cid}/attach", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public void recordWithAttachment(@PathVariable("cid") String cid, @RequestParam("record") byte[] recordb,
            @RequestParam("attached") byte[] attached) throws CollectionStateException {
        CollectionRecord record = null;
        try {
            record = convertFromB64(new ByteArrayInputStream(recordb));
        } catch (IOException e) {
            throw new CollectRuntimeException("Failed to convert body to record", e);
        }
        // check if collection exists
        if (!collectionMap.containsKey(cid)) {
            // collection not existing
            throw new CollectionStateException("Collection not existing");
        }
        // check if record is valid
        if (!recordValidator.validate(record)) {
            // record not valid
            throw new IllegalArgumentException("Record is invalid");
        }
        // store attachment
        try {
            String contentId = temporaryStorage.store(cid, new ByteArrayInputStream(attached));
            record.setAttachment(contentId);
            addRecord(cid, record);
        } catch (IOException e) {
            throw new CollectRuntimeException("Failed to save the attached file", e);
        }

    }

    @RequestMapping(value = "/records/{cid}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    public CollectionDetail getCollectionInfo(@PathVariable("cid") String cid) {
        Collection collection = collectionMap.get(cid);
        CollectionDetail detail = new CollectionDetail(collection);
        List<CollectionRecord> records = new ArrayList<CollectionRecord>();
        records.addAll(collectionRecords.get(cid));
        detail.setRecords(records);
        return detail;
    }

    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public class ResourceNotFoundException extends RuntimeException {
        public ResourceNotFoundException(String message) {
            super(message);
        }

        public ResourceNotFoundException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    @ResponseStatus(value = HttpStatus.NO_CONTENT)
    public class NoContentException extends RuntimeException {
        public NoContentException(String message) {
            super(message);
        }

        public NoContentException(String message, Throwable cause) {
            super(message, cause);
        }
    }

    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    public Exception handleIllegalArgumentException(IllegalArgumentException ex) {
        // clean stack trace from object
        ex.setStackTrace(new StackTraceElement[0]);
        return ex;
    }

    public CollectionRecord convertFromB64(InputStream body) throws IOException {
        CollectionRecord cr = new CollectionRecord();
        Base64InputStream is = new Base64InputStream(body);
        Map<String, Object> content = MAPPER.readValue(is, Map.class);
        cr.setContent(content);
        return cr;
    }

}