1
\$\begingroup\$

My assignment was to develop a REST service that will provide Airport information. This service is assumed to be used by multiple users, therefore data consistency (thread safety) and responding fast are the main concerns. Following you can see implementation details.

AirportData is the core data object of this service.

    /**
      * Represents a single airport
      */
    public class AirportData {
    /**
     * Constants for data validation
     */

    private static final int IATA_CODE_LENGTH = 3;
    private static final int ICAO_CODE_LENGTH = 4;

    private static final double MINIMUM_LATITUDE = -90.0;
    private static final double MAXIMUM_LATITUDE = 90.0;

    private static final double MINIMUM_longITUDE = -180.0;
    private static final double MAXIMUM_longITUDE = 180.0;

    private static final int MAX_SIGNIFICANT_DIGITS = 6;

    /** Name of the airport */
    private String name;

    /** Main city served by airport. May be spelled differently from name. */
    private String city;

    /** Country or territory where airport is located. */
    private String country;

    /** the three letter IATA code */
    private String iata;

    /** 4-letter ICAO code */
    private String icao;

    /** latitude value in degrees */
    private double latitude;

    /** longitude value in degrees */
    private double longitude;

    /** altitude value in feets */
    private long altitude;

    /** Hours offset from UTC. */
    private double timezone;

    /** Day light saving time of airport */
    private DST zone = DST.U;

    /**
     * Private constructor to prevent unwanted instantiation
     * Use Builder instead.
     * @param Airport data builder
     */
    private AirportData(Builder builder) {
        name = builder.name;
        city = builder.city;
        country = builder.country;
        iata = builder.iata;
        icao = builder.icao;
        latitude = builder.latitude;
        longitude = builder.longitude;
        altitude = builder.altitude;
        timezone = builder.timezone;
        zone = builder.zone;
    }

    public String getName() {
        return name;
    }

    public String getIata() {
        return iata;
    }

    public double getLatitude() {
        return latitude;
    }

    public double getLongitude() {
        return longitude;
    }

    public String getCity() {
        return city;
    }

    public String getCountry() {
        return country;
    }

    public String getIcao() {
        return icao;
    }

    public long getAltitude() {
        return altitude;
    }

    public double getTimezone() {
        return timezone;
    }

    public DST getZone() {
        return zone;
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }

    /**
     * Two airports are identical if their IATA codes are the same
     */
    @Override
    public boolean equals(Object other) {
        if(!(other instanceof AirportData))
            return Boolean.FALSE;
        AirportData otherAirport = (AirportData) other;
        return new EqualsBuilder().append(iata, otherAirport.getIata()).isEquals();
    }

    /**
     * Hash code is calculated only using IATA code of airport
     */
    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(iata).toHashCode();
    }

    /**
     * Builder to create airport data instances.
     * @author emiralp
     *
     */
    public static class Builder {
        private String name;
        private String city;        
        private String country;             
        private String iata;        
        private String icao;        
        private double latitude;
        private double longitude;        
        private long altitude;        
        private double timezone;        
        private DST zone;

        public Builder() { }

        public Builder withName(String name) {
            this.name = name;
            return this;
        }

        public Builder withCity(String city) {
            this.city = city;
            return this;
        }

        public Builder withCountry(String country) {
            this.country = country;
            return this;
        }

        public Builder withIATA(String iata) throws InvalidValueException {
            if(iata.trim().isEmpty() || iata.trim().length() == IATA_CODE_LENGTH) {
                this.iata = iata;
                return this;
            }

            throw new InvalidValueException(iata);
        }

        public Builder withICAO(String icao) throws InvalidValueException {
            if(icao.trim().isEmpty() || icao.trim().length() == ICAO_CODE_LENGTH) {
                this.icao = icao;
                return this;
            }

            throw new InvalidValueException(icao);
        }

        public Builder withLatitude(double latitude) throws InvalidValueException {
            if(latitude >= MINIMUM_LATITUDE && latitude <= MAXIMUM_LATITUDE && WeatherUtil.getSignificantDigitCount(latitude) <= MAX_SIGNIFICANT_DIGITS) {
                this.latitude = latitude;
                return this;
            }

            throw new InvalidValueException(latitude);
        }

        public Builder withLongitude(double longitude) throws InvalidValueException {
            if(longitude >= MINIMUM_longITUDE && longitude <= MAXIMUM_longITUDE && WeatherUtil.getSignificantDigitCount(longitude) <= MAX_SIGNIFICANT_DIGITS) {
                this.longitude = longitude;
                return this;
            } 

            throw new InvalidValueException(longitude);
        }

        public Builder withAltitude(long altitude) {
            this.altitude = altitude;
            return this;
        }

        public Builder withTimezone(double timezone) {
            this.timezone = timezone;
            return this;
        }

        public Builder withDSTZone(DST zone) {
            this.zone = zone;
            return this;
        }

        public AirportData build() {
            return new AirportData(this);
        }
    }
}

Atmospheric Information of a airport. An airport can have only one atmospheric information.

/**
 * encapsulates sensor information for a particular location
 */
public class AtmosphericInformation {

    /**
     * Valid Ranges for Atmospheric Data.
     */
    // Temperature ranges
    private static final int MIN_TEMP = -50;
    private static final int MAX_TEMP = 100;

    // Humidity ranges
    private static final int MIN_HUMIDITY = 0;
    private static final int MAX_HUMIDITY = 100;

    // Precipitation ranges
    private static final int MIN_PREC = 0;
    private static final int MAX_PREC = 100;

    // Pressure ranges
    private static final int MIN_PRESSURE = 650;
    private static final int MAX_PRESSURE = 800;

    // CloudCover ranges
    private static final int MIN_COVER = 0;
    private static final int MAX_COVER = 100;

    /** temperature in degrees celsius */
    private DataPoint temperature;

    /** wind speed in km/h */
    private DataPoint wind;

    /** humidity in percent */
    private DataPoint humidity;

    /** precipitation in cm */
    private DataPoint precipitation;

    /** pressure in mmHg */
    private DataPoint pressure;

    /** cloud cover percent from 0 - 100 (integer) */
    private DataPoint cloudCover;

    /** the last time this data was updated, in milliseconds since UTC epoch */
    /** this field is not considered in equals() and hashCode() methods since it's just a label */
    private long lastUpdateTime;

    private AtmosphericInformation(Builder builder) {
        this.temperature = builder.temperature;
        this.wind = builder.wind;
        this.humidity = builder.humidity;
        this.precipitation = builder.precipitation;
        this.pressure = builder.pressure;
        this.cloudCover = builder.cloudCover;
    }

    public DataPoint getTemperature() {
        return temperature;
    }
    public void setTemperature(DataPoint temperature) throws InvalidValueException {
        if(temperature.getMean() >= MIN_TEMP && temperature.getMean() < MAX_TEMP) {
            this.temperature = temperature;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(temperature);
    }
    public DataPoint getWind() {
        return wind;
    }
    public void setWind(DataPoint wind) throws InvalidValueException {
        if(wind.getMean() >= 0) {
            this.wind = wind;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(wind);
    }
    public DataPoint getHumidity() {
        return humidity;
    }
    public void setHumidity(DataPoint humidity) throws InvalidValueException {
        if(humidity.getMean() >= MIN_HUMIDITY && humidity.getMean() < MAX_HUMIDITY) {
            this.humidity = humidity;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(humidity);
    }
    public DataPoint getPrecipitation() {
        return precipitation;
    }
    public void setPrecipitation(DataPoint precipitation) throws InvalidValueException {
        if(precipitation.getMean() >= MIN_PREC && precipitation.getMean() < MAX_PREC) {
            this.precipitation = precipitation;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(precipitation);
    }
    public DataPoint getPressure() {
        return pressure;
    }
    public void setPressure(DataPoint pressure) throws InvalidValueException {
        if(pressure.getMean() >= MIN_PRESSURE && pressure.getMean() < MAX_PRESSURE) {
            this.pressure = pressure;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(pressure);
    }
    public DataPoint getCloudCover() {
        return cloudCover;
    }
    public void setCloudCover(DataPoint cloudCover) throws InvalidValueException {
        if(cloudCover.getMean() >= MIN_COVER && cloudCover.getMean() < MAX_COVER) {
            this.cloudCover = cloudCover;
            setLastUpdateTime();
            return;
        }

        throw new InvalidValueException(cloudCover);
    }
    public long getLastUpdateTime() {
        return this.lastUpdateTime;
    }

    private void setLastUpdateTime() {
        this.lastUpdateTime = System.currentTimeMillis();
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }

    @Override
    public boolean equals(Object other) {
        if (!(other instanceof AtmosphericInformation))
            return Boolean.FALSE;

        AtmosphericInformation atmInfo = (AtmosphericInformation) other;
        return new EqualsBuilder().append(temperature, atmInfo.getTemperature()).append(wind, atmInfo.getWind())
                .append(humidity, atmInfo.getHumidity()).append(precipitation, atmInfo.precipitation)
                .append(pressure, atmInfo.getPressure()).append(cloudCover, atmInfo.getCloudCover()).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(temperature).append(wind).append(humidity).append(precipitation)
                .append(pressure).append(cloudCover).toHashCode();
    }

    public static class Builder {
        private DataPoint temperature;        
        private DataPoint wind;
        private DataPoint humidity;
        private DataPoint precipitation;
        private DataPoint pressure;
        private DataPoint cloudCover;

        public Builder() { }

        public Builder withTemperature(DataPoint temperature) {
            this.temperature = temperature;
            return this;
        }

        public Builder withWind(DataPoint wind) {
            this.wind = wind;
            return this;
        }

        public Builder withHumidity(DataPoint humidity) {
            this.humidity = humidity;
            return this;
        }

        public Builder withPrecipitation(DataPoint precipitation) {
            this.precipitation = precipitation;
            return this;
        }

        public Builder withPressure(DataPoint pressure) {
            this.pressure = pressure;
            return this;
        }

        public Builder withCloudCover(DataPoint cloudCover) {
            this.cloudCover = cloudCover;
            return this;
        }

        public AtmosphericInformation build() {
            return new AtmosphericInformation(this);
        }
    }
}

Single atmospheric data. Think this class as the child of atmospheric information.

    /**
 * A collected point, including some information about the range of collected values
 *
 * 
 */
public class DataPoint {
     /** the mean of the observations */
    private double mean = 0.0;

    /** 1st quartile -- useful as a lower bound */
    private int first = 0;

    /** 2nd quartile -- median value */
    private int median = 0;

    /** 3rd quartile value -- less noisy upper value */
    private int last = 0;

    /** the total number of measurements */
    private int count = 0;

    /** Internal config flag to denote testing transient fields in equals and hashCode methods*/
    private static final boolean TEST_TRANSIENTS = false;

    private DataPoint(Builder builder) {
        this.first = builder.first;
        this.mean = builder.mean;
        this.median = builder.median;
        this.last = builder.last;
        this.count = builder.count;
    }

    public double getMean() {
        return mean;
    }

    public void setMean(double mean) { 
        this.mean = mean; 
    }

    public int getFirst() {
        return first;
    }

    public void setFirst(int first) {
        this.first = first;
    }

    public int getMedian() {
        return median;
    }

    public void setMedian(int median) {
        this.median = median;
    }

    public int getLast() {
        return last;
    }

    public void setLast(int last) {
        this.last = last;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toString(this, ToStringStyle.NO_CLASS_NAME_STYLE);
    }

    @Override
    public boolean equals(Object that) {
        return EqualsBuilder.reflectionEquals(this, that, TEST_TRANSIENTS);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, TEST_TRANSIENTS);
    }

    public static class Builder {
        private int first;
        private int mean;
        private int median;
        private int last;
        private int count;

        public Builder() { }

        public Builder withFirst(int first) {
            this.first= first;
            return this;
        }

        public Builder withMean(int mean) {
            this.mean = mean;
            return this;
        }

        public Builder withMedian(int median) {
            this.median = median;
            return this;
        }

        public Builder withCount(int count) {
            this.count = count;
            return this;
        }

        public Builder withLast(int last) {
            this.last = last;
            return this;
        }

        public DataPoint build() {
            return new DataPoint(this);
        }
    }
}

Enumeration for data point type. This is useful when converting request data.

/**
 * The various types of data points we can collect.
 *
 * 
 */
public enum DataPointType {
    WIND ("Wind"),
    TEMPERATURE ("Temperature"),
    HUMIDTY ("Humidity"),
    PRESSURE ("Pressure"),
    CLOUDCOVER ("CloudCover"),
    PRECIPITATION ("Precipitation");

    private String name;

    private DataPointType(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return this.name;
    }

    /**
     * Gets corresponding enum from given name.
     * The comparison is case insensitive, therefore "wind" and "WIND" will retrieve same enum.
     * @param name of the enumeration. 
     * @return enum
     * @throws DataPointUnknownException if no such enum. 
     */
    public static DataPointType fromName(String name) throws DataPointUnknownException {
        for(DataPointType type : values()) {
            if(type.name.equalsIgnoreCase(name))
                return type;
        }

        throw new DataPointUnknownException(name);
    }   
}

Daylight saving time enumeration. This is to outline the list of values and to ease of access.

/**
 * Daylight saving time zones
 * 
 * 
 *
 */
public enum DST {
    E ("Europe"),
    A ("US/Canada"),
    S ("South America"),
    O ("Australia"),
    Z ("New Zealand"),
    N ("None"),
    U ("Unknown");

    private String name;

    private DST(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return this.name;
    }

    /**
     * Gets corresponding DST zone from given name.
     * The comparison is case insensitive, therefore "europe" and "EUROPE" will retrieve same enum.
     * @param name
     * @return corresponding enum or Unknown (DST.U)
     */
    public DST fromName(String name) {
        for(DST zone : values()) {
            if(zone.name.equalsIgnoreCase(name))
                return zone;
        }
        return DST.U;
    }
}

The assignment requested to keep the data in-memory. This class have necessary collections (concurrent collections) to support.

/**
 * In-memory repository for Airport Weather Service APP. 
 * 
 *
 */
public class InMemoryRepository implements Repository {
    /** Initial capacity to support */
    private static final int DEFAULT_CAPACITY = 1000;


    /** airports and their atmospheric information, key corresponds with airportData 
     *
     * Since one airport can only have one AtmospherinInformation object
     * It is OK to combine them in a map.
     */
    private final Map<AirportData, AtmosphericInformation> airportMap;

    /**
     * Internal performance counter to better understand most requested information
     * 
     */
    private final Map<AirportData, AtomicInteger> requestFrequency;

    /**
     * Performance counter to measure radius 
     */
    private final Map<Double, AtomicInteger> radiusFrequency;

    /** Singleton instance */
    private static final Repository INSTANCE = new InMemoryRepository();

    /**
     * private constructor to prevent malicious instantiations.
     */
    private InMemoryRepository() {      
        airportMap = new ConcurrentHashMap<>(DEFAULT_CAPACITY);
        requestFrequency = new ConcurrentHashMap<>();
        radiusFrequency = new ConcurrentHashMap<>();
    }

    /**
     * Gets the singleton instance of this class
     * @return {@link InMemoryRepository} instance
     */
    public static Repository getInstance() {
        return INSTANCE;
    }   

    @Override
    public Map<AirportData, AtomicInteger> getRequestFrequency() {
        return requestFrequency;
    }

    @Override
    public Map<Double, AtomicInteger> getRadiusFrequency() {
        return radiusFrequency;
    }

    @Override
    public Map<AirportData, AtmosphericInformation> getAirports() {
        return airportMap;
    }

}

Service class for the application

    /**
     * Weather service for Airports
     * This service class implements {@code Service}
     * 
     *
     */
    public class WeatherService implements Service {

        /**
         * Initial counter value for request frequencies.
         * Counter starts from -1 since on every airport addition,
         * it will get initialized and incremented to 0.
         */
        private static final int INITIAL_REQUEST_COUNTER = -1;

        /**
         * Initial counter value for radius frequencies. 
         * This counter starts from 0 since it is initialized
         * when first request for radius is made. after counter
         * is initialized it will be incremented to 1. 
         */
        private static final int INITIAL_RADIUS_COUNTER = 0;

        /**
         * Minimum array size for radius frequency calculations.
         */
        private static final double MINIMUM_SIZE = 0.0;

        /** One day in milliseconds */
        private static final int DAY = 86400000;

        /** Denotes the airport itself */
        private static final double CENTER = 0.0;

        /** Divisor for calculating radius fractions */
        private static final int MOD = 10;

        /** Signal to send to underlying JVM */
        private static final int SHUTDOWN_SIGNAL = 0;

        /**
         * Reference to underlying data store.
         */
        private final Repository repository;

        /**
         * Protected constructor.
         * Only {@link ServiceFactory} classes should instantiate Service classes
         * 
         * @param repository to persist data updates
         */
        protected WeatherService(Repository repository) {
            this.repository = repository;
        }

        @Override
        public DataPointType getDataPointType(String pointType) throws DataPointUnknownException {
            return DataPointType.fromName(pointType);
        }

        @Override
        public void updateAtmosphericInformation(AtmosphericInformation ai, DataPointType type, DataPoint dp) throws DataPointUnknownException, InvalidValueException {

            // Update one of the Data points using switch
            // there is no default case since we are using all possible enumerations enumeration
            switch (type) {
            case WIND:
                ai.setWind(dp);
                break;
            case TEMPERATURE:
                ai.setTemperature(dp);
                break;
            case HUMIDTY:
                ai.setHumidity(dp);
                break;
            case PRESSURE:
                ai.setPressure(dp);
                break;
            case CLOUDCOVER:
                ai.setCloudCover(dp);
                break;
            case PRECIPITATION:
                ai.setPrecipitation(dp);
                break;
            default:
                throw new InvalidValueException(type);
            }     
        }

        @Override
        public void updateRadiusFrequency(double radius) {
            // if it is the first time radius is requested start the counter
            repository.getRadiusFrequency().computeIfAbsent(radius, key -> new AtomicInteger(INITIAL_RADIUS_COUNTER));
            // increment counter
            repository.getRadiusFrequency().get(radius).incrementAndGet();
        }

        @Override
        public void updateRequestFrequency(AirportData airport) {
            //if it is the first time airport is requested start the counter
            repository.getRequestFrequency().computeIfAbsent(airport, key -> new AtomicInteger(INITIAL_REQUEST_COUNTER));
            // increment counter
            repository.getRequestFrequency().get(airport).incrementAndGet();
        };

        @Override
        public Entry<AirportData, AtmosphericInformation> findAirportData(String iataCode) throws AirportNotFoundException {
            // two airports are equal if they have the same IATA code
            return repository.getAirports().entrySet().stream()
                .filter(entry -> entry.getKey().getIata().equalsIgnoreCase(iataCode))
                .findFirst().orElseThrow(() -> new AirportNotFoundException(iataCode));
        }

        @Override
        public long getDatasize() {
            // At least one atmospheric info should be added in a day
            return repository.getAirports().values().stream().filter(
                    info -> !WeatherUtil.isEmpty(info) && (info.getLastUpdateTime() > (System.currentTimeMillis() - DAY)))
                    .count();
        }

        @Override
        public Map<String, Double> calculateRequestFrequencies() {
            // first calculate how many airports do have counter
            // actually this is also the number of known airports in the system
            final int requestCount = repository.getRequestFrequency().size();

            // calculate fraction of requests
            return repository.getRequestFrequency().entrySet().stream().collect(
                    Collectors.toMap(e -> e.getKey().getIata(), e -> Double.valueOf(e.getValue().get()) / requestCount));
        }

        @Override
        public int [] calculateRadiusFrequencies() {
            // Get the integer value of highest radius queried
            final int m = repository.getRadiusFrequency().keySet().stream()
                    .max(Double::compare)
                    .orElse(MINIMUM_SIZE).intValue();
            // Make array size atomic for thread safety
            AtomicInteger size = new AtomicInteger(m);

            final int[] hist = new int[size.incrementAndGet()];
            // Do some unclear logic..
            repository.getRadiusFrequency().entrySet().stream().forEach(entry -> {
                int i = entry.getKey().intValue() % MOD;
                hist[i] += entry.getValue().intValue();
            });

            return hist;
        }

        @Override
        public List<AtmosphericInformation> getAtmosphericInformation(Entry<AirportData, AtmosphericInformation> airportEntry, double radius) {
            List<AtmosphericInformation> atmosphericData = new ArrayList<>();

            // If only weather of given airport is requested
            if (radius == CENTER) {
                if(!WeatherUtil.isEmpty(airportEntry.getValue()))               
                    atmosphericData.add(airportEntry.getValue());

            } else {
                // current airport and nearby airports weather is requested
                atmosphericData = repository.getAirports().entrySet().stream()
                        .filter(entry -> WeatherUtil.calculateDistance(airportEntry.getKey(), entry.getKey()) <= radius
                                && !WeatherUtil.isEmpty(entry.getValue()))
                        .map(entry -> entry.getValue()).collect(Collectors.toList());
            }

            return atmosphericData;
        }

        @Override
        public Set<String> getIATACodes() {
            return repository.getAirports().keySet().stream().map(key -> key.getIata()).collect(Collectors.toSet());
        }

        @Override
        public void deleteAirport(AirportData airport) throws AirportNotFoundException, InvalidValueException {     
            // Remove compares objects by invoking equals() method.
            // AirportData compares objects by looking at their IATA codes.
            // hence AirportData above may have a match in map.
            if(repository.getAirports().remove(airport) == null) 
                throw new AirportNotFoundException(airport.getIata());                              
        }

        @Override
        public void removeRequestFrequency(AirportData airport) throws AirportNotFoundException {
            AtomicInteger counter = repository.getRequestFrequency().remove(airport);
            if(counter == null)
                throw new AirportNotFoundException(airport.getIata());
        }

        @Override
        public void removeRadiusFrequency(double radius) throws InvalidValueException {
            AtomicInteger counter = repository.getRadiusFrequency().remove(radius);
            if(counter == null)
                throw new InvalidValueException(radius);
        }

        @Override
        public AirportData createAirport(String iata, String latitude, String longitude) throws InvalidValueException {
            // Start to build airport data object with IATA code.
            Builder airportBuilder = new AirportData.Builder().withIATA(iata);

            // latitude and longitude values are optional       
            if(StringUtils.isNotBlank(latitude))
                airportBuilder = airportBuilder.withLatitude(Double.valueOf(latitude));

            if(StringUtils.isNotBlank(longitude))
                airportBuilder = airportBuilder.withLongitude(Double.valueOf(longitude));

            return airportBuilder.build();      
        }

        @Override
        public AtmosphericInformation createAtmoshpericInformation () {
            return new AtmosphericInformation.Builder().build();
        }

        @Override
        public void addAirport(AirportData airport, AtmosphericInformation atmosphericInformation) throws AirportExistsException {      
            // If airport is already there (!= null) throw exception
            if(repository.getAirports().putIfAbsent(airport, atmosphericInformation) != null)
                throw new AirportExistsException(airport.getIata());
        }

        @Override
        public void clear() {       
            repository.getAirports().clear();       
            repository.getRequestFrequency().clear();
            repository.getRadiusFrequency().clear();                    
        }

        @Override
        public void exit() {
            System.exit(SHUTDOWN_SIGNAL);
        }
    }

Facade of the application. This ensures abstraction of endpoints from underlying service class.

/**
 * Facade class for weather service.
 * 
 * 
 *
 */
public class WeatherServiceFacade implements ServiceFacade {

    /**
     * Reference to repository layer.
     */
    private final Repository repository;

    /**
     * Reference to service layer.
     */
    private final Service service;

    /**
     *  shared gson json to object factory   
     */
    private static final Gson gson = new Gson();

    /** Keys to describe each statistics */
    private static final String STATS_SIZE_KEY = "datasize";
    private static final String STATS_IATA_FREQ_KEY = "iataFreq";
    private static final String STATS_RAD_FREQ_KEY = "radiusFreq";

    /**
     * Constructor that takes {@link RepositoryType} as parameter.
     * Default repository is IN-MEMORY repository.
     * 
     * {@link RepositoryType} is implemented for future improvements.
     * For now, AWS supports only In-Memory repository and therefore it
     * is not required to process parameter (inside switch or if blocks, etc.)
     * 
     * @param type of repository that this facade is providing access to. 
     */
    public WeatherServiceFacade(RepositoryType type) {      
        // setup facade. 
        repository = InMemoryRepository.getInstance();
        service = new WeatherService(repository);
    }

    @Override
    public String getStatistics() {

        final Map<String, Object> stats = new HashMap<>();

        final long datasize = service.getDatasize();        
        stats.put(STATS_SIZE_KEY, datasize);

        final Map<String, Double> frequencies = service.calculateRequestFrequencies();       
        stats.put(STATS_IATA_FREQ_KEY, frequencies);

        final int [] hist = service.calculateRadiusFrequencies();        
        stats.put(STATS_RAD_FREQ_KEY, hist);

        return gson.toJson(stats);
    }

    @Override
    public List<AtmosphericInformation> getAirportWeatherData(String iata, String radius) throws NumberFormatException, AirportNotFoundException {
        // get airport entry
        final Entry<AirportData, AtmosphericInformation> airportEntry = service.findAirportData(iata);
        // radius : convert String to double
        final double r = WeatherUtil.extractRadius(radius);

        // update counters
        service.updateRequestFrequency(airportEntry.getKey());        
        service.updateRadiusFrequency(r); 

        return service.getAtmosphericInformation(airportEntry, r);
    }

    @Override
    public void updateWeather(String iata, String pointType, String datapointJson) throws JsonParseException, AirportNotFoundException, DataPointUnknownException, InvalidValueException {      
        // get airport entry 
        final Entry<AirportData, AtmosphericInformation> airportEntry = service.findAirportData(iata);      

        final DataPointType type = service.getDataPointType(pointType); 
        // convert JSON String to actual object.
        final DataPoint dataPoint = gson.fromJson(datapointJson, DataPoint.class);

        // update airport's weather info
        service.updateAtmosphericInformation(airportEntry.getValue(), type, dataPoint);
    }

    @Override
    public Set<String> getIATACodes() {
        return service.getIATACodes();
    }

    @Override
    public AirportData getAirport(String iata) throws AirportNotFoundException {

        final Entry<AirportData, AtmosphericInformation> airportEntry = service.findAirportData(iata);          

        // we have a match, update request counter
        service.updateRequestFrequency(airportEntry.getKey());  

        return airportEntry.getKey();
    }

    @Override
    public void addAirport(String iata, String latitude, String longitude) throws PropertyMissingException, NumberFormatException, InvalidValueException, AirportExistsException {

        // Check if latitude and longitude are set
        if(StringUtils.isAnyBlank(latitude, longitude))
            throw new PropertyMissingException();

        // build new airport object from input
        final AirportData newAirport = service.createAirport(iata, latitude, longitude);
        // Create a empty atmosphericInfo object to bind it with airport.
        // Since an airport could have only one atmosphericInfo object
        // It is all right to do this.
        final AtmosphericInformation atmosphericInformation = service.createAtmoshpericInformation();
        // persist the new airport
        service.addAirport(newAirport, atmosphericInformation); 
        // initialize counter
        service.updateRequestFrequency(newAirport);
    }

    @Override
    public List<String> addAirports(String airportsJson) throws JsonParseException {
        final List<AirportData> airports = gson.fromJson(airportsJson, WeatherUtil.getAirportListType());       

        final List<String> failures = new ArrayList<>();

        // iterate over the list and add each airport
        airports.stream().forEach(airport -> {
            try {
                // Create a empty atmosphericInfo object to bind it with airport.
                // Since an airport could have only one atmosphericInfo object
                // It is all right to do this.
                final AtmosphericInformation atmosphericInformation = service.createAtmoshpericInformation();
                service.addAirport(airport, atmosphericInformation);
                // initialize counter
                service.updateRequestFrequency(airport);
            } catch (AirportExistsException e) {
                // Since this is batch processing, we will catch the error and 
                // log it to response.
                failures.add(e.getMessage());
            }
        });
        return failures;    
    }

    @Override
    public void deleteAirport(String iata) throws InvalidValueException, AirportNotFoundException {
        AirportData airport = service.createAirport(iata, null, null);
        service.deleteAirport(airport); 
        service.removeRequestFrequency(airport);
    }

    @Override
    public void exit() {
        service.exit();     
    }

    @Override
    public void clear() {
        service.clear();        
    }
}

Utility class of application. This class contains static methods that can be accessed directly from anywhere of application.

/**
 * Utility class for Airport Weather Service App.
 * 
 *
 */
public class WeatherUtil {

    /** earth radius in KM */
    private static final double RADIUS = 6372.8;

    /** Constants of Haversine algorithm */
    private static final int POWER = 2;
    private static final int DIVISOR = 2;
    private static final int MULTIPLIER = 2;

    /** Type for List<AirportData> */
    private static final Type AIRPORT_LIST_TYPE = new TypeToken<List<AirportData>>(){}.getType();

    /** denotes the airport it self in radius */
    private static final double CENTER_RADIUS = 0.0;

    /**
     * Haversine distance between two airports.
     *
     * @param ad1 first airport
     * @param ad2 second airport
     * @return the distance between airports in KM (kilometers).
     */
    public static double calculateDistance(AirportData ad1, AirportData ad2) {
        double deltaLat = Math.toRadians(ad2.getLatitude() - ad1.getLatitude());
        double deltaLon = Math.toRadians(ad2.getLongitude() - ad1.getLongitude());
        double a =  Math.pow(Math.sin(deltaLat / DIVISOR), POWER) + Math.pow(Math.sin(deltaLon / DIVISOR), POWER)
                * Math.cos(ad1.getLatitude()) * Math.cos(ad2.getLatitude());
        double c = MULTIPLIER * Math.asin(Math.sqrt(a));
        return RADIUS * c;
    }

    /**
     * Calculates number of significant digits for given double value
     * @param value
     * @return count of significant digits.
     */
    public static int getSignificantDigitCount(double value) {
        BigDecimal bd = new BigDecimal(String.valueOf(value));
        String plainVal = bd.stripTrailingZeros().toPlainString();
        int index = plainVal.indexOf(".");
        return index < 0 ? 0 : plainVal.length() - index - 1;
    }

    /**
     * Returns true if object is null or none of properties are set
     * @param info object to check
     * @return true if object is null or none of properties are set
     */
    public static boolean isEmpty(AtmosphericInformation info) {
        if(info == null)
            return Boolean.TRUE;

        return info.getCloudCover() == null && info.getHumidity() == null && info.getPressure() == null
                && info.getPrecipitation() == null && info.getTemperature() == null && info.getWind() == null;
    }

    public static double extractRadius(String radiusString) throws NumberFormatException {
        return radiusString == null || radiusString.trim().isEmpty() ? CENTER_RADIUS : Double.valueOf(radiusString);
    }

    /**
     * Gets the specific genericized type.
     * Used for deserializing json data.
     * @return type
     */
    public static Type getAirportListType() {
        return AIRPORT_LIST_TYPE;
    }
}

This is the special case of the requirement. It is assumed that a power user should be able to upload static data to the system directly. This class loads a CSV file, parses it to airport data objects and sends them in bulk to REST endpoint.

/**
 * A simple airport loader which reads a file from disk and sends entries to the web service
 *
 * IMPORTANT NOTE: It is assumed that file's absolute path will be provided hence {@link ClassLoader} is not preferred.
 *                 Relative paths may not work.
 * 
 *
 */
public class AirportLoader {

    private static final Logger LOGGER = Logger.getLogger(AirportLoader.class.getName());

    /** number of arguments this program expects to operate */
    private static final int ARGUMENT_SIZE = 1;

    /** the index of file path in arguments array */
    private static final int PATH_INDEX = 0;

    /** denotes empty file length */
    private static final long INVALID_FILE_SIZE = 0L;

    /** Separator for splitting String */
    private static final String CSV_SEPERATOR = ",";

    /** Each CSV line is expected to have this much value */
    private static final int VALID_FORMAT_LENGTH = 11;

    /** Constants to delete quotes from CSV file */
    private static final String QUOTE = "\"";
    private static final String EMPTY_STRING = "";

    /** end point to supply updates */
    private static WeatherCollectorEndpoint endpoint = new RestWeatherCollectorEndpoint();

    /** shared gson json to object factory */
    private static final Gson gson = new Gson();

    public AirportLoader() { }

    /**
     * Uploads airports to the server
     * @param airportDataStream the stream of airports
     * @throws IOException 
     */
    public void upload(InputStream airportDataStream) throws IOException {
        final List<AirportData> airports = new ArrayList<>();

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(airportDataStream))) {

            String line = null;        
            while ((line = reader.readLine()) != null) {

                // remove quotes from values.
                line = line.replace(QUOTE, EMPTY_STRING);

                // split values to assign them
                String [] values = line.split(CSV_SEPERATOR);

                // We just care the first eleven fields.
                if (values.length >= VALID_FORMAT_LENGTH) {

                    try {
                        // Build the object
                        AirportData airport = new AirportData.Builder().withName(values[1]).withCity(values[2])
                                .withCountry(values[3]).withIATA(values[4]).withICAO(values[5])
                                .withLatitude(Double.valueOf(values[6])).withLongitude(Double.valueOf(values[7]))
                                .withAltitude(Long.valueOf(values[8])).withTimezone(Double.valueOf(values[9]))
                                .withDSTZone(DST.valueOf(values[10])).build();

                        // Add it to list
                        airports.add(airport);
                    } catch (NumberFormatException | InvalidValueException e) {
                        LOGGER.log(Level.SEVERE, e.getMessage(), e);
                    } 

                } else {
                    LOGGER.severe("CSV line has invalid value count :" + values.length);
                }
            }
        } 

        sendAirports(airports);     
    }

    /**
     * Sends loaded airports to Server
     * @param airports
     */
    private void sendAirports(List<AirportData> airports) {     
        Response response = endpoint.addAirports(gson.toJson(airports));
        // It is ok to suppress
        @SuppressWarnings("unchecked")
        List<String> failures = (List<String>) response.getEntity();

        LOGGER.info("Errors:");
        failures.forEach(fail -> LOGGER.info(fail));
    }

    public static void main(String args[]) throws IOException {
        if(args == null || args.length < ARGUMENT_SIZE) {
            LOGGER.severe("Missing argument!");
            return;
        }
        File airportDataFile = new File(args[PATH_INDEX]);

        if (!airportDataFile.exists() || airportDataFile.length() == INVALID_FILE_SIZE) {
            LOGGER.severe(airportDataFile + " is not a valid input");
            return;
        }

        AirportLoader al = new AirportLoader();
        al.upload(new FileInputStream(airportDataFile));

        Response response = endpoint.getAirports();

        //  It is ok to suppress
        @SuppressWarnings("unchecked")
        Set<String> airports = (Set<String>) response.getEntity();
        LOGGER.info("Known airports after loading:");
        airports.stream().forEach(airport -> LOGGER.info(airport));

    }
}

/**
 * A REST implementation of the WeatherCollector API. Accessible only to airport
 * weather collection sites via secure VPN.
 *
 * @author code test administrator
 */
public class RestWeatherCollectorEndpoint implements WeatherCollectorEndpoint {

    /** logs errors */
    private static final Logger LOGGER = Logger.getLogger(RestWeatherCollectorEndpoint.class.getName());

    /** facade object to access service layer */
    private final ServiceFacade facade;

    /**
     * Constructor
     */
    public RestWeatherCollectorEndpoint() {
        facade = new WeatherServiceFacade(RepositoryType.IN_MEMORY);
    }

    @Override
    public Response ping() {
        return Response.status(Response.Status.OK).build();
    }

    @Override
    public Response updateWeather(String iataCode, String pointType, String datapointJson) {

        try {
            facade.updateWeather(iataCode, pointType, datapointJson);
            return Response.status(Response.Status.OK).build();
        } catch (JsonParseException | WeatherException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }

    }

    @Override
    public Response getAirports() {
        return Response.status(Response.Status.OK).entity(facade.getIATACodes()).build();
    }

    @Override
    public Response getAirport(String iata) {

        try {           
            return Response.status(Response.Status.OK).entity(facade.getAirport(iata)).build();         
        } catch (WeatherException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
        }

    }

    @Override
    public Response addAirport(String iata, String latString, String longString) {

        try {               
            facade.addAirport(iata, latString, longString);
            return Response.status(Response.Status.CREATED).build();            
        } catch (NumberFormatException | WeatherException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }

    @Override
    public Response deleteAirport(String iata) {

        try {
            facade.deleteAirport(iata);     
            return Response.status(Response.Status.OK).build();         
        } catch (WeatherException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return Response.status(Response.Status.NOT_FOUND).entity(e.getMessage()).build();
        }
    }

    @Override
    public Response exit() {        
        facade.exit();
        return Response.noContent().build();
    }

    @Override
    public Response addAirports(String airportsJson) {

        try {           
            return Response.status(Response.Status.CREATED).entity(facade.addAirports(airportsJson)).build();           
        } catch (JsonParseException e) {
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }
}

/**
 * The Weather App REST endpoint allows clients to query, update and check health stats. Currently, all data is
 * held in memory. The end point deploys to a single container
 *
 * @author code test administrator
 */
public class RestWeatherQueryEndpoint implements WeatherQueryEndpoint {

    private static final Logger LOGGER = Logger.getLogger(RestWeatherQueryEndpoint.class.getName());

    /** facade object to access service layer */
    private final ServiceFacade facade;

    /**
     * public constructor
     */
    public RestWeatherQueryEndpoint() {
        facade = new WeatherServiceFacade(RepositoryType.IN_MEMORY);
    }

    @Override
    public String ping() {      
        return facade.getStatistics();
    }

    @Override
    public Response weather(String iata, String radiusString) {

        try {
            return Response.status(Response.Status.OK).entity(facade.getAirportWeatherData(iata, radiusString)).build();            
        } catch (NumberFormatException | WeatherException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
            return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).build();
        }
    }
}

/**
 * A reference implementation for the weather client. Consumers of the REST API can look at WeatherClient
 * to understand API semantics. This existing client populates the REST end point with dummy data useful for
 * testing.
 *
 * @author code test administrator
 */
public class WeatherClient {

    private static final Logger LOGGER = Logger.getLogger(WeatherClient.class.getName());

    /** end point for read queries */
    private WebTarget query;

    /** end point to supply updates */
    private WebTarget collect;

    private static final String IATA_CODE = "BOS";
    private static final String RADIUS = "0";

    public WeatherClient() {
        Client client = ClientBuilder.newClient();
        query = client.target(Paths.UriPath.BASE_URI).path(Paths.UriPath.QUERY);
        collect = client.target(Paths.UriPath.BASE_URI).path(Paths.UriPath.COLLECT);
    }

    public void pingCollect() {
        WebTarget path = collect.path(Paths.UriPath.PING);
        Response response = path.request().get();
        LOGGER.info(response.getStatusInfo().getReasonPhrase());
    }

    public void query(String iata) {
        WebTarget path = query.path(Paths.UriPath.WEATHER).path(iata).path(RADIUS);
        Response response = path.request().get();
        LOGGER.info(response.readEntity(String.class));
    }

    public void pingQuery() {
        WebTarget path = query.path(Paths.UriPath.PING);
        Response response = path.request().get();
        LOGGER.info(response.readEntity(String.class));
    }

    public void populate(String pointType, int first, int last, int mean, int median, int count) {
        WebTarget path = collect.path(Paths.UriPath.WEATHER).path(IATA_CODE).path(pointType);
        DataPoint dp = new DataPoint.Builder().withFirst(first).withLast(last).withMean(mean).withMedian(median)
                .withCount(count).build();
        Response post = path.request().post(Entity.entity(dp, MediaType.APPLICATION_JSON));
        LOGGER.info(post.getStatusInfo().getReasonPhrase());
    }

    public void exit() {        
        try {
            collect.path(Paths.UriPath.EXIT).request().get();
        } catch (ProcessingException e) {
            // Empty catch block.
            // This is to safely complete invocation
            // calling exit endpoint will possibly throw ConnectException since underlying JVM of server will be shutdown.
        }

    }

    public static void main(String[] args) {
        WeatherClient wc = new WeatherClient();
        wc.pingCollect();
        wc.populate("wind", 0, 10, 6, 4, 20);

        wc.query("BOS");
        wc.query("JFK");
        wc.query("EWR");
        wc.query("LGA");
        wc.query("MMU");

        wc.pingQuery();
        wc.exit();
        LOGGER.info("Complete");
    }
}

/**
 * A main method used to test the Weather Application. You are free to modify this main method
 * as you wish - it's in not used by the grader.
 *
 * @author code test administrator
 */
public class WeatherServer {

    private static final Logger LOGGER = Logger.getLogger(WeatherServer.class.getName());

    /** Denotes if HTTP should start immediately after instantiation */
    private static final boolean START_IMMEDIATE = false;

    /** How long to wait to shut server down (in seconds) */
    private static final long SHUTDOWN_PERIOD = 1L;


    public static void main(String[] args) {
        try {
            LOGGER.info("Starting Weather App local testing server: " + Paths.UriPath.BASE_URI);
            LOGGER.info("Not for production use");

            init();

            final ResourceConfig resourceConfig = new ResourceConfig();
            resourceConfig.register(RestWeatherQueryEndpoint.class);
            resourceConfig.register(RestWeatherCollectorEndpoint.class);

            resourceConfig.register(JacksonFeature.class);

            final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(URI.create(Paths.UriPath.BASE_URI), resourceConfig, START_IMMEDIATE);

            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    LOGGER.info("Shutting down Weather Server.");
                    server.shutdown(SHUTDOWN_PERIOD, TimeUnit.SECONDS);
                }
            }));

            HttpServerProbe probe = new HttpServerProbe.Adapter() {
                @SuppressWarnings("rawtypes") 
                public void onRequestReceiveEvent(HttpServerFilter filter, Connection connection, Request request) {
                    System.out.println(request.getRequestURI());
                }
            };

            server.getServerConfiguration().getMonitoringConfig().getWebServerConfig().addProbes(probe);
            server.start();
            LOGGER.info("Weather Server started.");
            LOGGER.info(String.format("URL = %s", Paths.UriPath.BASE_URI));


            // blocks until exit
            Thread.currentThread().join();
            server.shutdown();
        } catch (IOException | InterruptedException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }
    }

   /**
    * A dummy init method that loads hard coded data
    *
    */
    protected static void init() {
        ServiceFacade facade = new WeatherServiceFacade(RepositoryType.IN_MEMORY);

        try {
            facade.addAirport("BOS", "42.364347", "-71.005181");
            facade.addAirport("EWR", "40.6925", "-74.168667");
            facade.addAirport("JFK", "40.639751", "-73.778925");
            facade.addAirport("LGA", "40.777245", "-73.872608");
            facade.addAirport("MMU", "40.79935", "-74.414874");
        } catch (WeatherException | NumberFormatException e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }
    }

}

Is the code above good enough in terms of thread-safety and efficiency ?

\$\endgroup\$
2
\$\begingroup\$

I think you may be overusing string constants. I know it's not good to use magic numbers and constants, especially if they are being used in multiple places. There are quite a few constants in your code which are only used once and I feel it would be better to use them directly (for contextual understanding of code) rather than naming constants for them. Btw, did this code get you the job?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.