public class GlobalSettings {
    /**
     * Used to set the global logging levels, like whether to log SQL queries or prints
     * inside methods.
     */
    public static final boolean DEBUGGING = true;

    /**
     * The BCrypt-hashed password field length (in User.class) is assumed to be 60 with a
     * 2-digit log factor. For example, in '$2a$10$...', the '10' is the log factor. If it
     * ever gets a 3-digit log factor (highly unlikely), the length of that field must
     * become 61.
     */
    public static final int BCRYPT_LOG_FACTOR = 10;

    /**
     * This regex matches if the given string contains a non-ASCII character. It, however,
     * does not match punctuation, so while "Hellö" matches this regex, "Hello!" won't.
     */
    public static final String NON_ASCII_REGEX = ".*[^\\p{ASCII}].*";


    /**
     * These strings provide the URIs of the controllers.
     */
    // User
    public static final String USER_PATH_MAPPING               = "/human";
    public static final String USER_CREATE_MAPPING             = "/register";
    public static final String USER_USERNAME_MAPPING           = USER_PATH_MAPPING + "/{username}";
    public static final String USER_SPECIFIC_MAPPING           = USER_USERNAME_MAPPING;
    // Project
    public static final String PROJECT_PATH_MAPPING            = USER_SPECIFIC_MAPPING + "/projects";
    public static final String PROJECT_ID_MAPPING              = "/{projectId}";
    public static final String PROJECT_CONNECTIONS_MAPPING     = PROJECT_ID_MAPPING + "/connections";
    public static final String PROJECT_REGIONS_MAPPING         = PROJECT_ID_MAPPING + "/regions";
    public static final String PROJECT_SPECIFIC_MAPPING        = PROJECT_PATH_MAPPING + PROJECT_ID_MAPPING;
    // Region
    public static final String REGION_PATH_MAPPING             = PROJECT_SPECIFIC_MAPPING + "/regions";
    public static final String REGION_ID_MAPPING               = "/{regionId}";
    public static final String REGION_SPECIFIC_MAPPING         = REGION_PATH_MAPPING + REGION_ID_MAPPING;
    public static final String REGION_MAP_IMAGE_MAPPING        = REGION_ID_MAPPING + "/mapimage";
    // Beacon
    public static final String BEACON_PATH_MAPPING             = REGION_SPECIFIC_MAPPING + "/beacons";
    public static final String BEACON_ID_MAPPING               = "/{beaconId}";
    public static final String BEACON_SPECIFIC_MAPPING         = BEACON_PATH_MAPPING + BEACON_ID_MAPPING;
    public static final String BEACON_CONNECTION_MAPPING       = BEACON_ID_MAPPING + "/connection";
    // Scenario
    public static final String SCENARIO_PATH_MAPPING           = PROJECT_SPECIFIC_MAPPING + "/scenarios";
    public static final String SCENARIO_ID_MAPPING             = "/{scenarioId}";
    public static final String SCENARIO_SPECIFIC_MAPPING       = SCENARIO_PATH_MAPPING + SCENARIO_ID_MAPPING;
    public static final String SCENARIO_MEMBER_BEACONS_MAPPING = SCENARIO_ID_MAPPING + "/beacons";
    public static final String SCENARIO_ADD_BEACON_MAPPING     = SCENARIO_ID_MAPPING + "/addbeacon";
    public static final String SCENARIO_REMOVE_BEACON_MAPPING  = SCENARIO_ID_MAPPING + "/removebeacon";

    // API Root
    public static final String API_PATH_MAPPING                      = "/robot";
    // API Sub
    public static final String API_PROJECT_QUERY_PATH_MAPPING        = "/project";
    public static final String API_REGION_QUERY_PATH_MAPPING         = "/regions";
    public static final String API_CONNECTION_QUERY_PATH_MAPPING     = "/connections";
    public static final String API_REGION_IMG_QUERY_PATH_MAPPING     = API_REGION_QUERY_PATH_MAPPING + "/{regionId}/image";
    public static final String API_CONNECTION_IMG_QUERY_PATH_MAPPING = API_CONNECTION_QUERY_PATH_MAPPING + "/{connectionId}/image";
    public static final String API_BEACON_QUERY_PATH_MAPPING         = API_REGION_QUERY_PATH_MAPPING + "/{regionId}/beacons";
    public static final String API_BEACON_INFO_QUERY_PATH_MAPPING    = API_BEACON_QUERY_PATH_MAPPING + "/{beaconId}/info";


    /**
     * These strings provide the file storage locations.
     */
    public static final String USER_HOME_FOLDER_PATH               = System.getProperty("user.home");
    public static final String ROOT_STORAGE_FOLDER_DIRECTORY_NAME  = "ibeacon-server-storage";
    public static final String IMAGE_STORAGE_FOLDER_DIRECTORY_NAME = "image";
    public static final String IMAGE_STORAGE_FOLDER_PATH           = USER_HOME_FOLDER_PATH + "/" +
            ROOT_STORAGE_FOLDER_DIRECTORY_NAME + "/" +
            IMAGE_STORAGE_FOLDER_DIRECTORY_NAME + "/";
    public static final String TEXT_STORAGE_FOLDER_DIRECTORY_NAME  = "text";
    public static final String TEXT_STORAGE_FOLDER_PATH            = USER_HOME_FOLDER_PATH + "/" +
            ROOT_STORAGE_FOLDER_DIRECTORY_NAME + "/" +
            TEXT_STORAGE_FOLDER_DIRECTORY_NAME + "/";

    /**
     * These strings provide package names for annotation-based scanning.
     */
    public static final String BASE_PACKAGE_NAME       = "com.aemreunal";
    // This must be the path of the package which holds the entity classes.
    // The Entity Manager Factory scans the package designated by this string
    // to map entities.
    public static final String ENTITY_PACKAGE_NAME     = BASE_PACKAGE_NAME + ".domain";
    public static final String REPOSITORY_PACKAGE_NAME = BASE_PACKAGE_NAME + ".repository";


    //-------------------------------------------------------------------------------------------
    // Property name: "hibernate.hbm2ddl.auto"
    //
    // hibernate.hbm2ddl.auto: Automatically validates or exports schema DDL to the
    // database when the SessionFactory is created. With create-drop, the database
    // schema will be dropped when the SessionFactory is closed explicitly.
    //
    // Values: "validate" | "update" | "create" | "create-drop"
    //
    // validate:    validate the schema, makes no changes to the database.
    // update:      update the schema.
    // create:      creates the schema, destroying previous data.
    // create-drop: drop the schema at the end of the session.
    //----------------------------------------
    public static final String HBM2DDL_KEY      = "hibernate.hbm2ddl.auto";
    public static final String HBM2DDL_PROPERTY = "update";
    //-------------------------------------------------------------------------------------------


    //-------------------------------------------------------------------------------------------
    // Property name: "hibernate.show_sql"
    //----------------------------------------
    public static final String SHOW_SQL_KEY      = "hibernate.show_sql";
    public static final String SHOW_SQL_PROPERTY = String.valueOf(DEBUGGING);
    //-------------------------------------------------------------------------------------------


    //-------------------------------------------------------------------------------------------
    // Property name: "hibernate.format_sql"
    //----------------------------------------
    public static final String FORMAT_SQL_KEY      = "hibernate.format_sql";
    public static final Object FORMAT_SQL_PROPERTY = "true";
    //-------------------------------------------------------------------------------------------


    //-------------------------------------------------------------------------------------------
    // Property name: "hibernate.dialect"
    //----------------------------------------
    public static final String DB_DIALECT_KEY      = "hibernate.dialect";
    public static final String DB_DIALECT_PROPERTY = "org.hibernate.dialect.MySQL5InnoDBDialect";
    //-------------------------------------------------------------------------------------------


    //-------------------------------------------------------------------------------------------
    // Property name: "Multipart max upload size"
    //
    // This value limits the maximum Multipart upload size to the designated
    // value. Files larger than this will be rejected.
    //----------------------------------------
    public static final long MAX_UPLOAD_SIZE_BYTES = 1572864; // 1572864 Bytes (= 1.5 MB)
    //-------------------------------------------------------------------------------------------

    /**
     * Security authentication queries.
     */
    public static final String USERS_FOR_AUTH_QUERY    = "SELECT username, password, '1' FROM users WHERE username = ?";
    public static final String AUTHORITY_OF_USER_QUERY = "SELECT username, 'ADMIN' FROM users WHERE username = ?";

    /**
     * System flag for whether to allow HTTP traffic
     */
    public static final String IBEACON_HTTP_ALLOW_KEY = "IBEACON_HTTP_ALLOW";

    public static void log(String log) {
        if (GlobalSettings.DEBUGGING) {
            System.out.println(log);
        }
    }

    public static void err(String log) {
        System.err.println(log);
    }
} @SuppressWarnings("UnusedDeclaration")

@Entity
@Table(name = "beacons")
@ResponseBody
@JsonIgnoreProperties(value = { "project", "region", "connections", "locationInfoTextFileName" })
public class Beacon extends ResourceSupport implements Serializable, Comparable {
    // UUID hex string (including dashes) is 36 characters long
    public static final int UUID_MAX_LENGTH         = 36;
    public static final int DISPLAY_NAME_MAX_LENGTH = 50;
    public static final int DESCRIPTION_MAX_LENGTH  = 200;

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'ID' attribute
     */
    @Id
    @Column(name = "beacon_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Access(AccessType.PROPERTY)
    private Long beaconId;
    /*
     * END: Beacon 'ID' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'UUID' attribute
     */
    @Column(name = "uuid", nullable = false, length = UUID_MAX_LENGTH)
    @Size(min = UUID_MAX_LENGTH, max = UUID_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String uuid = "";
    /*
     * END: Beacon 'UUID' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'Major' attribute
     */
    @Column(name = "major", nullable = false)
    @Access(AccessType.PROPERTY)
    private Integer major = -1;
    /*
     * END: Beacon 'Major' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'Minor' attribute
     */
    @Column(name = "minor", nullable = false)
    @Access(AccessType.PROPERTY)
    private Integer minor = -1;
    /*
     * END: Beacon 'Minor' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'description' attribute
     */
    @Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
    @Size(max = DESCRIPTION_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String description = "";
    /*
     * END: Beacon 'description' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'display name' attribute
     *
     * A 'display name' is a user-friendly text that can be displayed on a client as
     * the name of this beacon/location. For example, the beacon description may be
     * "beacon 5, under northwest light switch", which isn't a user-friendly way to
     * describe a location. This text can be "Room 353", which can be displayed to a
     * user to describe the area this beacon represents.
     */
    @Column(name = "display_name", nullable = false, length = DISPLAY_NAME_MAX_LENGTH)
    @Size(max = DISPLAY_NAME_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String displayName = "";
    /*
     * END: Beacon 'display name' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'region' attribute
     */
    @ManyToOne(targetEntity = Region.class,
            fetch = FetchType.LAZY,
            optional = true)
    // JoinTable & Lazy fetch-> 5.1.7: http://docs.jboss.org/hibernate/core/4.3/manual/en-US/html_single/
    @JoinTable(name = "regions_to_beacons",
            joinColumns = @JoinColumn(name = "beacon_id"),
            inverseJoinColumns = @JoinColumn(name = "region_id"))
    @Access(AccessType.PROPERTY)
    private Region region;
    /*
     * END: Beacon 'region' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'X coordinate' attribute
     */
    @Column(name = "x_coordinate", nullable = false)
    @Access(AccessType.PROPERTY)
    private Integer xCoordinate;
    /*
     * END: Beacon 'X coordinate' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'Y coordinate' attribute
     */
    @Column(name = "y_coordinate", nullable = false)
    @Access(AccessType.PROPERTY)
    private Integer yCoordinate;
    /*
     * END: Beacon 'Y coordinate' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'scenario' attribute
     */
    @ManyToOne(targetEntity = Scenario.class,
            fetch = FetchType.LAZY,
            optional = false)
    @JoinTable(name = "scenarios_to_beacons",
            joinColumns = @JoinColumn(name = "beacon_id"),
            inverseJoinColumns = @JoinColumn(name = "scenario_id"))
    @Access(AccessType.PROPERTY)
    private Scenario scenario;
    /*
     * END: Beacon 'scenario' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'designated' attribute
     */
    @Column(name = "designated", nullable = false)
    @Access(AccessType.PROPERTY)
    private Boolean designated = false;
    /*
     * END: Beacon 'designated' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'location info' attribute
     *
     * The length is UUID_MAX_LENGTH, because text file names are set as UUIDs.
     */
    @Column(name = "location_info_text_file_name", length = UUID_MAX_LENGTH)
    @Size(min = UUID_MAX_LENGTH, max = UUID_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String locationInfoTextFileName = null;
    /*
     * END: Beacon 'location info' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'connections' attribute
     */
    @ManyToMany(targetEntity = Connection.class,
            fetch = FetchType.LAZY)
    @JoinTable(name = "beacons_to_connections",
            joinColumns = @JoinColumn(name = "beacon_id"),
            inverseJoinColumns = @JoinColumn(name = "connection_id"))
    @OrderBy("connectionId")
    @Access(AccessType.PROPERTY)
    private Set<Connection> connections = new LinkedHashSet<Connection>();
    /*
     * END: Beacon 'connections' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Beacon 'creationDate' attribute
     */
    @Column(name = "creation_date", nullable = false)
    @Access(AccessType.PROPERTY)
    private Date creationDate = null;
    /*
     * END: Beacon 'creationDate' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Constructors
     */
    public Beacon() {
        // Empty constructor for Spring & Hibernate
    }
    /*
     * END: Constructors
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Helpers
     */
    public void addConnection(Connection connection) {
        this.getConnections().add(connection);
    }

    public void removeConnection(Connection connection) {
        this.getConnections().remove(connection);
    }

    @JsonSerialize
    public boolean hasLocationInfo() {
        return getLocationInfoTextFileName() != null;
    }

    // Weird stuff: when this method is named something like 'getConnectionsJson',
    // 'getConnectionsList', 'getConnectionsAsList', Jackson tries to use it and
    // causes a LazyInit exception.
    @JsonIgnore
    public JSONArray getConnsAsJson() {
        JsonArrayBuilder connArray = JsonBuilderFactory.array();
        for (Connection connection : getConnections()) {
            connArray.add(connection.getBeaconIdsAsJson());
        }
        return connArray.build();
    }

    public JSONObject getQueryResponse() {
        return JsonBuilderFactory.object()
                                 .add("beaconId", getBeaconId())
                                 .add("uuid", getUuid())
                                 .add("major", getMajor())
                                 .add("minor", getMinor())
                                 .add("designated", getDesignated())
                                 .add("xCoordinate", getxCoordinate())
                                 .add("yCoordinate", getyCoordinate())
                                 .add("hasLocationInfo", hasLocationInfo())
                                 .add("displayName", getDisplayName())
                                 .build();
    }
    /*
     * END: Helpers
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Getters & Setters
     */
    public Long getBeaconId() {
        return beaconId;
    }

    public void setBeaconId(Long beaconId) {
        this.beaconId = beaconId;
    }

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid.toUpperCase();
    }

    public Integer getMajor() {
        return major;
    }

    public void setMajor(Integer major) {
        this.major = major;
    }

    public Integer getMinor() {
        return minor;
    }

    public void setMinor(Integer minor) {
        this.minor = minor;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public Region getRegion() {
        return region;
    }

    public void setRegion(Region region) {
        this.region = region;
    }

    public Integer getxCoordinate() {
        return xCoordinate;
    }

    public void setxCoordinate(Integer xCoordinate) {
        this.xCoordinate = xCoordinate;
    }

    public Integer getyCoordinate() {
        return yCoordinate;
    }

    public void setyCoordinate(Integer yCoordinate) {
        this.yCoordinate = yCoordinate;
    }

    public Scenario getScenario() {
        return scenario;
    }

    public void setScenario(Scenario scenario) {
        this.scenario = scenario;
    }

    public Boolean getDesignated() {
        return designated;
    }

    public Boolean isDesignated() {
        return designated;
    }

    public void setDesignated(Boolean designated) {
        this.designated = designated;
    }

    public String getLocationInfoTextFileName() {
        return locationInfoTextFileName;
    }

    public void setLocationInfoTextFileName(String locationInfoTextFileName) {
        this.locationInfoTextFileName = locationInfoTextFileName;
    }

    public Set<Connection> getConnections() {
        return connections;
    }

    public void setConnections(Set<Connection> connections) {
        this.connections = connections;
    }

    public Date getCreationDate() {
        return creationDate;
    }

    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }
    /*
     * END: Getters & Setters
     *------------------------------------------------------------
     */

    @PrePersist
    private void setInitialProperties() {
        // Set beacon creation date
        if (creationDate == null) {
            setCreationDate(new Date());
        }
    }

    @Override
    public String toString() {
        return "[Beacon: " + getBeaconId() + ", Region: "
                + getRegion().getRegionId() + ", Project: "
                + getRegion().getProject().getProjectId() + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Beacon) {
            return ((Beacon) obj).getBeaconId().equals(this.getBeaconId());
        }
        return false;
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof Beacon) {
            return this.getBeaconId().compareTo(((Beacon) o).getBeaconId());
        }
        return 0;
    }
} @Entity
@Table(name = "connections")
@ResponseBody
@JsonIgnoreProperties(value = { "project", "beacons", "connectionImageFileName" })
public class Connection extends ResourceSupport implements Serializable, Comparable {
    // UUID hex string (including dashes) is 36 characters long
    public static final int UUID_MAX_LENGTH = 36;

    /*
     *------------------------------------------------------------
     * BEGIN: Connection 'ID' attribute
     */
    @Id
    @Column(name = "connection_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Access(AccessType.PROPERTY)
    private Long connectionId;
    /*
     * END: Connection 'ID' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Connection 'Image file name' attribute
     *
     * The length is UUID_MAX_LENGTH, because image names are set as UUIDs.
     */
    @Column(name = "connection_image_name", nullable = false, length = UUID_MAX_LENGTH)
    @Size(min = UUID_MAX_LENGTH, max = UUID_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String connectionImageFileName = "";
    /*
     * END: Connection 'Image file name' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Connection 'beacons' attribute
     */
    @ManyToMany(targetEntity = Beacon.class,
            mappedBy = "connections",
            fetch = FetchType.LAZY)
    @Access(AccessType.PROPERTY)
    @OrderBy("beaconId")
    private Set<Beacon> beacons = new LinkedHashSet<Beacon>();
    /*
     * END: Connection 'beacons' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Connection 'project' attribute
     */
    @ManyToOne(targetEntity = Project.class,
            optional = false,
            fetch = FetchType.LAZY)
    @JoinTable(name = "projects_to_connections",
            joinColumns = @JoinColumn(name = "connection_id"),
            inverseJoinColumns = @JoinColumn(name = "project_id"))
    @Access(AccessType.PROPERTY)
    private Project project;
    /*
     * END: Connection 'project' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Helpers
     */
    public void addBeacon(Beacon beacon) {
        this.getBeacons().add(beacon);
    }

    public JSONArray getBeaconIdsAsJson() {
        return JsonBuilderFactory.array()
                                 .addAll(getBeacons().stream()
                                                     .map(Beacon::getBeaconId)
                                                     .collect(Collectors.toList()))
                                 .build();
    }

    public JSONObject getQueryResponse() {
        return JsonBuilderFactory.object()
                                 .add("connectionId", getConnectionId())
                                 .add("beacons", getBeaconIdsAsJson())
                                 .build();
    }
    /*
     * END: Helpers
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Constructors
     */
    public Connection() {
        // Empty constructor for Spring & Hibernate
    }
    /*
     * END: Constructors
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Getters & Setters
     */
    public Long getConnectionId() {
        return connectionId;
    }

    public void setConnectionId(Long connectionId) {
        this.connectionId = connectionId;
    }

    public String getConnectionImageFileName() {
        return connectionImageFileName;
    }

    public void setConnectionImageFileName(String connectionImageFileName) {
        this.connectionImageFileName = connectionImageFileName;
    }

    public Set<Beacon> getBeacons() {
        return beacons;
    }

    public void setBeacons(Set<Beacon> beacons) {
        this.beacons = beacons;
    }

    public Project getProject() {
        return project;
    }

    public void setProject(Project project) {
        this.project = project;
    }
    /*
     * END: Getters & Setters
     *------------------------------------------------------------
     */

    @Override
    public String toString() {
        return "[Connection: " + getConnectionId() +
                ", Project: " + getProject().getProjectId() + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Connection) {
            return ((Connection) obj).getConnectionId().equals(this.getConnectionId());
        }
        return false;
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof Connection) {
            return this.getConnectionId().compareTo(((Connection) o).getConnectionId());
        }
        return 0;
    }
} @Entity
@Table(name = "projects")
@ResponseBody
@JsonIgnoreProperties(value = { "beacons", "regions", "scenarios", "projectSecret", "owner", "connections" })
public class Project extends ResourceSupport implements Serializable, Comparable {
    public static final int NAME_MAX_LENGTH        = 50;
    public static final int DESCRIPTION_MAX_LENGTH = 200;
    // The BCrypt-hashed secret field length is assumed to be 60 with a
    // 2-digit log factor. For example, in '$2a$10$...', the '10' is the log
    // factor. If it ever gets a 3-digit log factor (highly unlikely), the
    // length of this field must become 61.
    public static final int BCRYPT_HASH_LENGTH     = 60;

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'ID' attribute
     */
    @Id
    @Column(name = "project_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Access(AccessType.PROPERTY)
    private Long projectId;
    /*
     * END: Project 'ID' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'name' attribute
     */
    @Column(name = "name", nullable = false, length = NAME_MAX_LENGTH)
    @Size(min = 1, max = NAME_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String name = "";
    /*
     * END: Project 'name' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'description' attribute
     */
    @Column(name = "description", nullable = false, length = DESCRIPTION_MAX_LENGTH)
    @Size(max = DESCRIPTION_MAX_LENGTH)
    @Access(AccessType.PROPERTY)
    private String description = "";
    /*
     * END: Project 'description' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'creationDate' attribute
     */
    @Column(name = "creation_date", nullable = false)
    @Access(AccessType.PROPERTY)
    private Date creationDate = null;
    /*
     * END: Project 'creationDate' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'owner' attribute
     */
    @ManyToOne(targetEntity = User.class,
            fetch = FetchType.LAZY,
            optional = false)
    @JoinTable(name = "users_to_projects",
            joinColumns = @JoinColumn(name = "project_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id"))
    @Access(AccessType.PROPERTY)
    private User owner;

    // TODO other users
    /*
     * END: Project 'owner' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'region list' attribute
     */
    @OneToMany(targetEntity = Region.class,
            mappedBy = "project",
            orphanRemoval = true,
            fetch = FetchType.LAZY,
            cascade = CascadeType.REMOVE)
    @OrderBy(value = "regionId")
    @Access(AccessType.PROPERTY)
    private Set<Region> regions = new LinkedHashSet<>();
    /*
     * END: Project 'region list' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'connection list' attribute
     */
    @OneToMany(targetEntity = Connection.class,
            mappedBy = "project",
            orphanRemoval = true,
            fetch = FetchType.LAZY,
            cascade = CascadeType.REMOVE)
    @OrderBy(value = "connectionId")
    @Access(AccessType.PROPERTY)
    private Set<Connection> connections = new LinkedHashSet<>();
    /*
     * END: Project 'connection list' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'scenarios list' attribute
     */
    @OneToMany(targetEntity = Scenario.class,
            mappedBy = "project",
            orphanRemoval = true,
            fetch = FetchType.LAZY,
            cascade = CascadeType.REMOVE)
    @OrderBy(value = "scenarioId")
    @Access(AccessType.PROPERTY)
    private Set<Scenario> scenarios = new LinkedHashSet<>();
    /*
     * END: Project 'scenarios list' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Project 'secret' attribute
     *
     * TODO add resetting secret
     */
    @Column(name = "project_secret", nullable = false, unique = false)
    @Size(min = BCRYPT_HASH_LENGTH, max = BCRYPT_HASH_LENGTH)
    @Access(AccessType.PROPERTY)
    private String projectSecret = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
    /*
     * END: Project 'secret' attribute
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Constructors
     */
    public Project() {
        // Empty constructor for Spring & Hibernate
    }
    /*
     * END: Constructors
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Helpers
     */
    public JSONObject getCreateResponse(String projectSecret) {
        return JsonBuilderFactory.object()
                                 .add("projectId", getProjectId())
                                 .add("name", getName())
                                 .add("description", getDescription())
                                 .add("secret", projectSecret)
                                 .add("links", getLinks())
                                 .build();
    }

    public JSONObject getQueryResponse() {
        return JsonBuilderFactory.object()
                                 .add("projectId", getProjectId())
                                 .add("links", getLinks())
                                 .build();
    }
    /*
     * END: Helpers
     *------------------------------------------------------------
     */

    /*
     *------------------------------------------------------------
     * BEGIN: Getters & Setters
     */
    public Long getProjectId() {
        return projectId;
    }

    public void setProjectId(Long projectId) {
        this.projectId = projectId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Date getCreationDate() {
        return creationDate;
    }

    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }

    public User getOwner() {
        return owner;
    }

    public void setOwner(User owner) {
        this.owner = owner;
    }

    public Set<Region> getRegions() {
        return regions;
    }

    public void setRegions(Set<Region> regions) {
        this.regions = regions;
    }

    public Set<Connection> getConnections() {
        return connections;
    }

    public void setConnections(Set<Connection> connections) {
        this.connections = connections;
    }

    public Set<Scenario> getScenarios() {
        return scenarios;
    }

    public void setScenarios(Set<Scenario> scenarios) {
        this.scenarios = scenarios;
    }

    public String getProjectSecret() {
        return projectSecret;
    }

    public void setProjectSecret(String projectSecret) {
        this.projectSecret = projectSecret;
    }
    /*
     * END: Getters & Setters
     *------------------------------------------------------------
     */

    @PrePersist
    private void setInitialProperties() {
        // Set project creation date
        if (creationDate == null) {
            setCreationDate(new Date());
        }
    }

    @Override
    public String toString() {
        return "[Project: " + getProjectId() +
                ", User: " + getOwner().getUsername() + "]";
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Project) {
            return ((Project) obj).getProjectId().equals(this.getProjectId());
        }
        return false;
    }

    @Override
    public int compareTo(Object o) {
        if (o instanceof Project) {
            return this.getProjectId().compareTo(((Project) o).getProjectId());
        }
        return 0;
    }
} public class ConnectionExistsException extends Exception {
    public ConnectionExistsException(Long beaconOneId, Long beaconTwoId) {
        super("There already is a connection between Beacon " + beaconOneId + " and Beacon " + beaconTwoId + "!");
    }
} public class ConnectionNotFoundException extends Exception {
    public ConnectionNotFoundException() {
        super("The requested connection can not be found.");
    }
} public class ConnectionNotPossibleException extends Exception {
    public ConnectionNotPossibleException(Long beaconId) {
        super("Unable to create connection, beacon " + beaconId + " is not a designated beacon!");
    }

    public ConnectionNotPossibleException() {
        super("Unable to create connection, a beacon can't be connected to itself!");
    }
} public class ImageDeleteException extends Exception {
    public ImageDeleteException(Long projectId, Long regionId) {
        super("Unable to delete the image file of project " + projectId + ", region " + regionId + "!");
    }
} public class ImageLoadException extends Exception {
    public ImageLoadException(Long projectId, Long regionId) {
        super(getExceptionMessage(projectId, regionId));
    }

    private static String getExceptionMessage(Long projectId, Long regionId) {
        if (regionId == null) {
            return "Unable to load the image file of project " + projectId + "!";
        } else {
            return "Unable to load the image file of project " + projectId + ", region " + regionId + "!";
        }
    }
} public class ImageSaveException extends Exception {
    public ImageSaveException(Long projectId, Long regionId) {
        super(createCauseMessage(projectId, regionId));
    }

    public static String createCauseMessage(Long projectId, Long regionId) {
        String message = "Unable to save ";
        if (regionId != null) {
            return message + "map image of project " + projectId + ", region " + regionId + "!";
        } else {
            return message + "a navigation image of project " + projectId + "!";
        }
    }
} public class MultipartFileReadException extends Exception {
    public MultipartFileReadException(Long projectId, Long regionId) {
        super("Unable to read the Multipart file, uploaded as the map image of project " + projectId + ", region " + regionId + "!");
    }
} public class WrongFileTypeSubmittedException extends Exception {
    public WrongFileTypeSubmittedException(Long projectId, Long regionId) {
        super("The submitted file, uploaded as the map image of project " + projectId + ", region " + regionId + ", is of a wrong type! Please submit an image file.");
    }
} public class ImageProperties {
    private final String imageFileName;
    private final int    imageWidth;
    private final int    imageHeight;

    public ImageProperties(String imageFileName, int imageWidth, int imageHeight) {
        this.imageFileName = imageFileName;
        this.imageWidth = imageWidth;
        this.imageHeight = imageHeight;
    }

    public String getImageFileName() {
        return imageFileName;
    }

    public int getImageWidth() {
        return imageWidth;
    }

    public int getImageHeight() {
        return imageHeight;
    }
} public class ImageStorage {

    /**
     * Saves the given image to the filesystem and returns the properties of the saved
     * image file as an {@link ImageProperties ImageProperties} object.
     * <p>
     * The method saves images under the home folder of the user, inside the:<pre>
     * ~/{@value com.aemreunal.config.GlobalSettings#ROOT_STORAGE_FOLDER_DIRECTORY_NAME}/{@value
     * com.aemreunal.config.GlobalSettings#IMAGE_STORAGE_FOLDER_DIRECTORY_NAME}/</pre>
     * folder. The regular region images will be put in a sub-folder structure like:<pre>
     * &lt;project ID&gt;/&lt;region ID&gt;/</pre> under the main
     * storage folder. Inter-region navigation images will be put directly under
     * the:<pre>
     * &lt;user name&gt;/&lt;project ID&gt;/</pre> folder, without any region-specific
     * foldering.
     * <p>
     * If any of the folders above do not exist, they will be created.
     *
     * @param projectId
     *         The ID of the project which the region (the image belongs to) is a part
     *         of.
     * @param regionId
     *         The ID of the region of the image. When saving an inter-region navigation
     *         connection image, this value should be {@code null}.
     * @param imageMultipartFile
     *         The image as a {@link MultipartFile MultipartFile}.
     *
     * @return The properties of the saved image file as an {@link ImageProperties
     * ImageProperties} object. These properties are: <ul> <li>Image name</li> <li>Image
     * width</li> <li>Image height</li> </ul>
     *
     * @throws MultipartFileReadException
     *         If the {@link MultipartFile Multipart file} can't be read.
     * @throws ImageSaveException
     *         If the image can't be saved.
     * @throws WrongFileTypeSubmittedException
     *         If the submitted {@link MultipartFile Multipart file} is of a wrong type
     *         (e.g. non-image file, unacceptable image type).
     */
    public ImageProperties saveImage(Long projectId, Long regionId, MultipartFile imageMultipartFile)
            throws MultipartFileReadException, ImageSaveException, WrongFileTypeSubmittedException {
        // Verify file is not empty or is not of a wrong type
        verifyImageType(projectId, regionId, imageMultipartFile);

        // Get the file path from the project ID and region ID attributes
        String filePath = getFilePath(projectId, regionId);

        // Ensure unique file name
        File imageFile = getUniqueFile(filePath);

        // Check for the existence of the parent folder and create it
        // if it doesn't exist
        createParentFolder(projectId, regionId, imageFile);

        // Create the new image file
        createFile(projectId, regionId, imageFile);

        // Write the image bytes to the new file
        writeImageToFile(projectId, regionId, imageMultipartFile, imageFile);

        // Read the image properties to get dimensions
        return readImageProperties(projectId, regionId, imageFile);
    }

    /**
     * Loads the specified image file and returns the bytes of the image file in a
     * <code>byte[]</code>.
     *
     * @param projectId
     *         The ID of the project which the region (the image belongs to) is a part
     *         of.
     * @param regionId
     *         The ID of the region of the image. When saving an inter-region navigation
     *         connection image, this value should be {@code null}.
     * @param imageFileName
     *         The name of the image file to be loaded.
     *
     * @return A <code>byte[]</code> if the image file has been successfully loaded and
     * read, <code>null</code> otherwise.
     *
     * @throws ImageLoadException
     *         If the image can't be loaded.
     */
    public byte[] loadImage(Long projectId, Long regionId, String imageFileName)
            throws ImageLoadException {
        // Get the file path from the project ID and region ID attributes
        String filePath = getFilePath(projectId, regionId);
        // Get the image file
        File imageFile = new File(filePath + imageFileName);
        if (!imageFile.exists()) {
            System.err.println("Image file does not exist!");
            throw new ImageLoadException(projectId, regionId);
        }
        return loadImageFromFile(projectId, regionId, imageFile);
    }

    /**
     * Deletes the specified image file. The method will do nothing if a {@code null}
     * value is provided for {@code imageFileName} parameter.
     *
     * @param projectId
     *         The ID of the project which the region (the image belongs to) is a part
     *         of.
     * @param regionId
     *         The ID of the region of the image.
     * @param imageFileName
     *         The name of the image file to be deleted. If a {@code null} value is
     *         provided, the method will do nothing and return.
     *
     * @throws ImageDeleteException
     *         If the image file can't be deleted.
     */
    public void deleteImage(Long projectId, Long regionId, String imageFileName)
            throws ImageDeleteException {
        if (imageFileName == null || imageFileName.equals("")) {
            return;
        }
        // Get the file path from the project ID and region ID attributes
        String filePath = getFilePath(projectId, regionId);
        // Get the image file
        File imageFile = new File(filePath + imageFileName);
        try {
            Files.delete(imageFile.toPath());
        } catch (NoSuchFileException e) {
            GlobalSettings.err("WARNING: Image file for project: "
                                       + projectId + ", region " + regionId + ", file name: "
                                       + imageFileName + " does not exist, nothing to delete!");
        } catch (IOException e) {
            GlobalSettings.err("Unable to delete the image!");
            throw new ImageDeleteException(projectId, regionId);
        }
    }

    private void verifyImageType(Long projectId, Long regionId, MultipartFile imageMultipartFile) throws WrongFileTypeSubmittedException {
        if (imageMultipartFile.isEmpty() || !fileTypeIsImage(imageMultipartFile)) {
            throw new WrongFileTypeSubmittedException(projectId, regionId);
        }
    }

    private boolean fileTypeIsImage(MultipartFile file) {
        String type = file.getContentType();
        return type.equalsIgnoreCase(MediaType.IMAGE_JPEG_VALUE);
    }

    private File getUniqueFile(String filePath) {
        File imageFile;
        do {
            String fileName = UUID.randomUUID().toString();
            imageFile = new File(filePath + fileName);
        } while (imageFile.exists());
        return imageFile;
    }

    private void createParentFolder(Long projectId, Long regionId, File imageFile) throws ImageSaveException {
        if (!imageFile.getParentFile().exists()) {
            // If it doesn't exist, create it
            if (!imageFile.getParentFile().mkdirs()) {
                System.err.println("Unable to create parent folders!");
                throw new ImageSaveException(projectId, regionId);
            }
        }
    }

    private void createFile(Long projectId, Long regionId, File imageFile) throws ImageSaveException {
        try {
            if (!imageFile.createNewFile()) {
                System.err.println("Unable to createNewFile()!");
                throw new ImageSaveException(projectId, regionId);
            }
        } catch (IOException e) {
            System.err.println("Unable to create file!");
            throw new ImageSaveException(projectId, regionId);
        }
    }

    private void writeImageToFile(Long projectId, Long regionId, MultipartFile imageMultipartFile, File imageFile) throws ImageSaveException {
        try {
            imageMultipartFile.transferTo(imageFile);
        } catch (IOException e) {
            System.err.println("Unable to write image to file!");
            throw new ImageSaveException(projectId, regionId);
        }
    }

    private ImageProperties readImageProperties(Long projectId, Long regionId, File imageFile)
    throws ImageSaveException {
        try {
            BufferedImage image = ImageIO.read(imageFile);
            return new ImageProperties(imageFile.getName(), image.getWidth(), image.getHeight());
        } catch (IOException e) {
            System.err.println("Unable to read image to get dimensions!");
            throw new ImageSaveException(projectId, regionId);
        }
    }

    private byte[] loadImageFromFile(Long projectId, Long regionId, File imageFile) throws ImageLoadException {
        // Can safely cast from long to int, as image file will never exceed
        // 2^32 bytes (which would've required the use of a 64 bit long).
        byte[] imageAsBytes = new byte[(int) imageFile.length()];
        try {
            FileInputStream stream = new FileInputStream(imageFile);
            stream.read(imageAsBytes);
            stream.close();
        } catch (FileNotFoundException e) {
            System.err.println("File to read from is not found!");
            throw new ImageLoadException(projectId, regionId);
        } catch (IOException e) {
            System.err.println("Unable to read from file!");
            throw new ImageLoadException(projectId, regionId);
        }
        return imageAsBytes;
    }

    private String getFilePath(Long projectId, Long regionId) {
        if (regionId != null) {
            return GlobalSettings.IMAGE_STORAGE_FOLDER_PATH + projectId.toString() + "/" + regionId.toString() + "/";
        } else {
            return GlobalSettings.IMAGE_STORAGE_FOLDER_PATH + projectId.toString() + "/";
        }
    }
} @Repository
public interface ConnectionRepo extends PagingAndSortingRepository<Connection, Long>, JpaSpecificationExecutor {
    Set<Connection> findByProject(Project project);

    Connection findByConnectionIdAndProject(Long connectionId, Project project);
} public class ConnectionSpecs {
    public static Specification<Connection> connectionWithSpecification(final Long projectId, final Beacon beaconOne, final Beacon beaconTwo) {
        return (root, query, builder) -> {
            ArrayList<Predicate> predicates = new ArrayList<Predicate>();
            // Project specification
            predicates.add(builder.equal(root.get("project").get("projectId"), projectId));

            if (beaconOne != null) {
                predicates.add(builder.isMember(beaconOne, root.get("beacons").as(Set.class)));
            }

            if (beaconTwo != null) {
                predicates.add(builder.isMember(beaconTwo, root.get("beacons").as(Set.class)));
            }

            return builder.and(predicates.toArray(new Predicate[predicates.size()]));
        };
    }

}
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.aemreunal.config.GlobalSettings;
import com.aemreunal.domain.Beacon;
import com.aemreunal.domain.Connection;
import com.aemreunal.domain.Project;
import com.aemreunal.exception.connection.ConnectionExistsException;
import com.aemreunal.exception.connection.ConnectionNotFoundException;
import com.aemreunal.exception.connection.ConnectionNotPossibleException;
import com.aemreunal.exception.imageStorage.ImageDeleteException;
import com.aemreunal.exception.imageStorage.ImageLoadException;
import com.aemreunal.exception.imageStorage.ImageSaveException;
import com.aemreunal.exception.region.MultipartFileReadException;
import com.aemreunal.exception.region.WrongFileTypeSubmittedException;
import com.aemreunal.helper.ImageProperties;
import com.aemreunal.helper.ImageStorage;
import com.aemreunal.repository.connection.ConnectionRepo;
import com.aemreunal.repository.connection.ConnectionSpecs;
package com.aemreunal.service;

/*
 * *********************** *
 * Copyright (c) 2015      *
 *                         *
 * This code belongs to:   *
 *                         *
 * @author Ahmet Emre Ünal *
 * S001974                 *
 *                         *
 * aemreunal@gmail.com     *
 * emre.unal@ozu.edu.tr    *
 *                         *
 * aemreunal.com           *
 * *********************** *
 */


@Transactional
@Service
public class ConnectionService {
    @Autowired
    private ConnectionRepo connectionRepo;

    @Autowired
    private BeaconService beaconService;

    @Autowired
    private ProjectService projectService;

    @Autowired
    private ImageStorage imageStorage;

    private Connection save(Connection connection) {
        return connectionRepo.save(connection);
    }

    public Connection createNewConnection(String username, Long projectId, Long beaconOneId, Long regionOneId, Long beaconTwoId, Long regionTwoId, MultipartFile imageMultipartFile)
    throws ConnectionExistsException, WrongFileTypeSubmittedException, ImageDeleteException, MultipartFileReadException, ImageSaveException, ConnectionNotPossibleException {