Java tutorial
/* * 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; } }