diff --git a/.gitignore b/.gitignore index e3307f4..a468cff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .classpath .settings/ bin/ +config/ # IntelliJ .idea diff --git a/pom.xml b/pom.xml index f6ae7b1..0664469 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ v8.9.1 UTF-8 UTF-8 - 1.0.0.Final + 1.2.1.Final 3.4.1.Final 2.22.0 v1.3.2 @@ -84,6 +84,10 @@ quarkus-mailer ${quarkus.version} + + org.jboss.resteasy + resteasy-multipart-provider + diff --git a/src/main/frontend/src/app/components/article-edit.js b/src/main/frontend/src/app/components/article-edit.js index 4197edb..cbe1f48 100644 --- a/src/main/frontend/src/app/components/article-edit.js +++ b/src/main/frontend/src/app/components/article-edit.js @@ -200,7 +200,7 @@ export default class ArticleEdit extends Component { } //if it is an event let eventInputs = ""; - if(this.state.location) { + if(this.state.tag === "events") { eventInputs = (
diff --git a/src/main/frontend/src/app/components/mobiletags.js b/src/main/frontend/src/app/components/mobiletags.js new file mode 100644 index 0000000..8d27c59 --- /dev/null +++ b/src/main/frontend/src/app/components/mobiletags.js @@ -0,0 +1,17 @@ +import React, {Component} from "react"; +import ApiCall from "../services/api-call"; +import Tags from "./tags"; + +export default class MobileTags extends Tags { + render() { + return ( + this.state.tags.map((tag, i) => { + let link = "/" + tag.name; + return
  • this.handleLinkClick(e, link)}>{tag.name} +
  • + }) + ); + } +} + diff --git a/src/main/frontend/src/app/components/sponsor-edit.js b/src/main/frontend/src/app/components/sponsor-edit.js new file mode 100644 index 0000000..43b314b --- /dev/null +++ b/src/main/frontend/src/app/components/sponsor-edit.js @@ -0,0 +1,151 @@ +import React, {Component} from "react"; +import ApiCall from "../services/api-call"; + +export default class SponsorEdit extends Component { + + constructor(props) { + super(props); + + this.state = { + sponsor: "", + web_url: "", + logo: null, + image_type: "", + message: null, + preview: null, + }; + + this.onSubmit = this.onSubmit.bind(this); + // this.goToRegister = this.goToRegister.bind(this); + } + + onChange = e => this.setState({[e.target.name]: e.target.value}); + + onFileChangeHandler = (e) => { + this.setState({ + logo: e.target.files[0], + image_type: e.target.files[0].type, + preview: URL.createObjectURL(e.target.files[0]) + }); + }; + + componentWillReceiveProps() { + const sponsorId = this.props.routeParams.sponsorId; + if (sponsorId === 'new-sponsor') { + this.setState({ + id: null, + sponsor: "", + web_url: "", + logo: null, + image_type: "", + preview: null, + }) + } else { + ApiCall.get("/api/sponsor/" + sponsorId) + .then((response) => this.setState( + { + id: response.data.id, + sponsor: response.data.name, + web_url: response.data.url, + preview: "/api/sponsor/logo/" + response.data.id + } + )); + } + } + + onSubmit(event) { + event.preventDefault(); + + const formData = new FormData(); + if (this.state.id) { + formData.append("id", this.state.id) + } + formData.append("sponsor", this.state.sponsor); + formData.append("web_url", this.state.web_url); + if (this.state.logo) { + formData.append("image_type", this.state.image_type); + formData.append("logo", this.state.logo); + } + + if (!(this.state.sponsor && this.state.web_url)) { + this.setState({message: "Please fill all fields."}); + } else { + let self = this; + //update sponsor + if (this.state.id) { + ApiCall.put("/api/sponsor", formData) + .then((response) => { + console.log(response.headers); + self.setState({message: "Sponsor with ID: " + this.state.id + " updated!"}); + self.props.router.push("/"); + }) + .catch(function (error) { + // handle error + console.log(error); + self.setState({message: "Ops... nothing happened (check the browser console)." + error.response.status}); + }); + } else { + //create sponsor + ApiCall.post("/api/sponsor", formData) + .then((response) => { + console.log(response.headers); + self.setState({message: "New Sponsor Created!"}); + self.props.router.push("/"); + }) + .catch(function (error) { + // handle error + console.log(error); + self.setState({message: "Ops... nothing happened (check the browser console)." + error.response.status}); + }); + } + } + } + + render() { + + let error = null; + + if (this.state.message) { + error = ( +
    + {this.state.message} +
    ); + } + + return ( +
    +
    +
    +
    +

    Create Sponsor

    + {error} +
    +
    + + +
    +
    + + +
    +
    + + + +
    +   +
    +
    +
    +
    +
    + ); + } +} \ No newline at end of file diff --git a/src/main/frontend/src/app/components/sponsor.js b/src/main/frontend/src/app/components/sponsor.js new file mode 100644 index 0000000..6961543 --- /dev/null +++ b/src/main/frontend/src/app/components/sponsor.js @@ -0,0 +1,49 @@ +import React, {Component} from "react"; +import JwtUtil from "../services/jwt-util"; + +export default class Sponsor extends Component { + constructor(props) { + super(props); + this.hashHistory = this.props.hashHistory; + } + + componentDidMount() { + } + + handleSponsorEdit(event, id) { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth' + }); + this.hashHistory.push("/edit-sponsor/" + id); + } + + handleSponsorRemove(event, id) { + // this.props.router.push("/remove-sponsor/" + id); + } + + render() { + let cursorPointerStyle = { + cursor: "pointer" + }; + + let editSponsor = JwtUtil.isCurrentUserAdmin() ? + ( + this.handleSponsorEdit(e, this.props.sponsor.id)}>Edit + this.handleSponsorRemove(e, this.props.sponsor.id)}>Delete + ) : ''; + + return ( +
    +
    + {this.props.sponsor.name}/ + {editSponsor}
    +
    + ) + } +} \ No newline at end of file diff --git a/src/main/frontend/src/app/components/sponsors.js b/src/main/frontend/src/app/components/sponsors.js new file mode 100644 index 0000000..c8cf829 --- /dev/null +++ b/src/main/frontend/src/app/components/sponsors.js @@ -0,0 +1,83 @@ +import React, {Component} from "react"; +import ApiCall from "../services/api-call"; +import JwtUtil from "../services/jwt-util"; +import Sponsor from "./sponsor"; + +export default class Sponsors extends Component { + constructor(props) { + super(props); + this.state = {sponsors: []}; + this.hashHistory = this.props.hashHistory; + } + + loadSingleSponsorById = (id) => { + ApiCall.get("/api/sponsor/" + id) + .then((response) => this.setState( + { + sponsors: [response.data] + } + )); + }; + + loadSponsors = () => { + ApiCall.get("/api/sponsor") + .then((response) => this.setState( + { + sponsors: response.data + } + )); + }; + + componentWillMount() { + this.loadSponsors(); + } + + handlerSponsorAdd(event) { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'smooth' + }); + this.hashHistory.push("/add-sponsor/new-sponsor"); + } + + render() { + let cursorPointerStyle = { + cursor: "pointer" + }; + + let sponsors = this.state.sponsors.map((sponsor, i) => { + return + }); + + let idx = 0; + let sponsor_rows = []; + let i; + for (i = 0; i < sponsors.length; i += 5) { + sponsor_rows[idx++] =
    + {sponsors.slice(i, Math.max(i + 5, sponsors.length))} +
    + } + + return ( +
    + {JwtUtil.isCurrentUserAdmin() ? +
    +
    +
    this.handlerSponsorAdd(e)}>Add Sponsor
    +
    +
    : '' + } +
    +
    +

    Our Sponsors

    +
    +
    + {sponsor_rows} +
    + ) + } +} \ No newline at end of file diff --git a/src/main/frontend/src/app/components/tags.js b/src/main/frontend/src/app/components/tags.js index 078d05d..fa4a85c 100644 --- a/src/main/frontend/src/app/components/tags.js +++ b/src/main/frontend/src/app/components/tags.js @@ -13,7 +13,7 @@ export default class Tags extends Component { // })}; } - componentWillMount() { + componentDidMount() { let self = this; let articles = ApiCall.get("/api/tag") .then((response) => this.setState({tags: response.data})) diff --git a/src/main/frontend/src/app/services/api-call.js b/src/main/frontend/src/app/services/api-call.js index 8a17bf3..e017f3c 100644 --- a/src/main/frontend/src/app/services/api-call.js +++ b/src/main/frontend/src/app/services/api-call.js @@ -36,7 +36,7 @@ export default class ApiCall { static getRestUrl() { if (!baseUrl) { - baseUrl = "http://localhost:8080"; + baseUrl = window.location.origin; } return baseUrl; diff --git a/src/main/frontend/src/index.js b/src/main/frontend/src/index.js index 69b3cd6..d05020f 100644 --- a/src/main/frontend/src/index.js +++ b/src/main/frontend/src/index.js @@ -1,7 +1,8 @@ import React from "react"; import ReactDOM from "react-dom"; -import Articles from "./app/components/articles"; +import Articles, {articles} from "./app/components/articles"; import Tags from "./app/components/tags"; +import MobileTags from "./app/components/mobiletags"; import {hashHistory, Route, Router} from 'react-router' import TagsFooter from "./app/components/tags-footer"; import CfpSubmit from "./app/components/cfp"; @@ -9,12 +10,17 @@ import Login from "./app/components/login"; import Registration from "./app/components/registration"; import SidebarPosts from "./app/components/sidebar-posts"; import ArticleEdit from "./app/components/article-edit"; +import SponsorEdit from "./app/components/sponsor-edit"; +import Sponsors from "./app/components/sponsors"; ReactDOM.render( , document.querySelector('#tags')); +ReactDOM.render( + + , document.querySelector('.slicknav_nav')); const Routing = () => ( @@ -26,6 +32,8 @@ const Routing = () => ( + + ); @@ -35,5 +43,7 @@ ReactDOM.render(, document ReactDOM.render(, document.querySelector('#tab2')); +ReactDOM.render(, document.querySelector('#sponsors_list')); + ReactDOM.render(, document.querySelector('#footer-menu')); diff --git a/src/main/java/bg/jug/website/cms/service/EventService.java b/src/main/java/bg/jug/website/cms/service/EventService.java index 17d9786..543333c 100644 --- a/src/main/java/bg/jug/website/cms/service/EventService.java +++ b/src/main/java/bg/jug/website/cms/service/EventService.java @@ -53,13 +53,13 @@ public Response updateEvent(@Valid Event event) { if (persisted == null) { return Response.status(Response.Status.NOT_FOUND).build(); - } else { - EntityUtils.updateEntity(persisted, event); - replaceTagsWithExistingOnes(event); - //Eager fetching. Otherwise page will not serialize - persisted.getTags().size(); - return Response.ok(persisted).build(); } + + EntityUtils.updateEntity(persisted, event); + replaceTagsWithExistingOnes(event); + //Eager fetching. Otherwise page will not serialize + persisted.getTags().size(); + return Response.ok(persisted).build(); } @DELETE diff --git a/src/main/java/bg/jug/website/cms/service/TagAwareService.java b/src/main/java/bg/jug/website/cms/service/TagAwareService.java index 430b95e..3401a82 100644 --- a/src/main/java/bg/jug/website/cms/service/TagAwareService.java +++ b/src/main/java/bg/jug/website/cms/service/TagAwareService.java @@ -13,22 +13,26 @@ * Base class for services dealing with tag relations */ public class TagAwareService { + protected void replaceTagsWithExistingOnes(Article article) { - if (article.getTags() != null && !article.getTags().isEmpty()) { - Set tagsToPersist = new HashSet<>(); - article.getTags() - .forEach(possiblyNewTag -> - { - List existingTags = Tag.find(Tag.FIND_BY_NAME, possiblyNewTag.getName()).page( - Page.of(0, 1)).list(); - if(existingTags != null && !existingTags.isEmpty()) { - Tag existingTag = existingTags.get(0); - tagsToPersist.add(existingTag); - } else { - tagsToPersist.add(possiblyNewTag); - } - }); - article.setTags(tagsToPersist); + if (article.getTags() == null || article.getTags().isEmpty()) { + return; + } + + Set tagsToPersist = new HashSet<>(); + article.getTags().forEach(possiblyNewTag -> replaceTag(tagsToPersist, possiblyNewTag)); + article.setTags(tagsToPersist); + } + + private void replaceTag(Set tagsToPersist, Tag possiblyNewTag) { + List existingTags = + Tag.find(Tag.FIND_BY_NAME, possiblyNewTag.getName()).page(Page.of(0, 1)).list(); + + if (existingTags != null && !existingTags.isEmpty()) { + Tag existingTag = existingTags.get(0); + tagsToPersist.add(existingTag); + } else { + tagsToPersist.add(possiblyNewTag); } } } diff --git a/src/main/java/bg/jug/website/core/util/CryptUtils.java b/src/main/java/bg/jug/website/core/util/CryptUtils.java index 7629ede..17e0abd 100644 --- a/src/main/java/bg/jug/website/core/util/CryptUtils.java +++ b/src/main/java/bg/jug/website/core/util/CryptUtils.java @@ -2,21 +2,22 @@ import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.security.Key; public class CryptUtils { private static final String KEY = "lkjq9q91jaq*9!l#"; - public static String encryptPassword(String password) { + public static byte[] encryptPassword(String password) { Key aesKey = new SecretKeySpec(KEY.getBytes(), "AES"); try { Cipher cipher = Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, aesKey); - byte[] encrypted = cipher.doFinal(password.getBytes()); - return new String(encrypted); + return cipher.doFinal(password.getBytes()); } catch (Exception e) { - return password; + return password.getBytes(StandardCharsets.UTF_8); } } diff --git a/src/main/java/bg/jug/website/sponsors/model/Sponsor.java b/src/main/java/bg/jug/website/sponsors/model/Sponsor.java new file mode 100644 index 0000000..b6d7373 --- /dev/null +++ b/src/main/java/bg/jug/website/sponsors/model/Sponsor.java @@ -0,0 +1,65 @@ +package bg.jug.website.sponsors.model; + +import javax.json.bind.annotation.JsonbTransient; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import bg.jug.website.core.model.AbstractEntity; + +@Entity +public class Sponsor extends AbstractEntity { + + @NotNull + @Size(min = 1, max = 150) + @Column(length = 150) + private String name; + + @NotNull + @Size(min = 1, max = 200) + @Column(length = 200) + private String url; + + @Column(length = 100) + @JsonbTransient + private String type; + + @JsonbTransient + @Lob + private byte[] logo; + + public Sponsor() { + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public byte[] getLogo() { + return logo; + } + + public void setLogo(byte[] logo) { + this.logo = logo; + } +} diff --git a/src/main/java/bg/jug/website/sponsors/service/SponsorService.java b/src/main/java/bg/jug/website/sponsors/service/SponsorService.java new file mode 100644 index 0000000..9d219a1 --- /dev/null +++ b/src/main/java/bg/jug/website/sponsors/service/SponsorService.java @@ -0,0 +1,119 @@ +package bg.jug.website.sponsors.service; + +import javax.annotation.security.RolesAllowed; +import javax.enterprise.context.RequestScoped; +import javax.transaction.Transactional; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.*; +import java.util.List; +import java.util.Map; + +import bg.jug.website.sponsors.model.Sponsor; +import org.jboss.resteasy.plugins.providers.multipart.InputPart; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; + +import static javax.ws.rs.core.Response.Status.BAD_REQUEST; +import static javax.ws.rs.core.Response.Status.NOT_FOUND; + +@RequestScoped +@Path("/sponsor") +@Produces(MediaType.APPLICATION_JSON) +public class SponsorService { + + @POST + @RolesAllowed("admin") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Transactional + public Response createSponsor(MultipartFormDataInput input) { + Sponsor sponsor = new Sponsor(); + + try { + updateSponsorFieldsFromInputData(input, sponsor); + } catch (IOException e) { + return Response.serverError().entity(e.getMessage()).build(); + } + sponsor.persistAndFlush(); + + return Response.ok().build(); + } + + private void updateSponsorFieldsFromInputData(MultipartFormDataInput input, Sponsor sponsor) + throws IOException { + Map> paramsMap = input.getFormDataMap(); + String name = paramsMap.get("sponsor").get(0).getBodyAsString(); + String url = paramsMap.get("web_url").get(0).getBodyAsString(); + sponsor.setName(name); + sponsor.setUrl(url); + + if (paramsMap.containsKey("image_type")) { + fillSponsorImageFields(sponsor, paramsMap); + } + } + + private void fillSponsorImageFields(Sponsor sponsor, Map> paramsMap) + throws IOException { + + String type = paramsMap.get("image_type").get(0).getBodyAsString(); + InputStream logoStream = paramsMap.get("logo").get(0).getBody(InputStream.class, null); + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + + int length = logoStream.read(buffer); + while (length > 0) { + baos.write(buffer, 0, length); + length = logoStream.read(buffer); + } + sponsor.setType(type); + sponsor.setLogo(baos.toByteArray()); + } + } + + @PUT + @RolesAllowed("admin") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Transactional + public Response updateSponsor(MultipartFormDataInput input) { + Map> paramsMap = input.getFormDataMap(); + + if (!paramsMap.containsKey("id")) { + return Response.status(BAD_REQUEST).entity("missing ID").build(); + } + + try { + long id = paramsMap.get("id").get(0).getBody(Long.class, null); + Sponsor sponsor = Sponsor.findById(id); + if (sponsor == null) { + return Response.status(NOT_FOUND).build(); + } + + updateSponsorFieldsFromInputData(input, sponsor); + } catch (IOException e) { + Response.serverError().entity(e.getMessage()).build(); + } + + return Response.ok().build(); + } + + @GET + public Response allSponsors() { + List allSubmissions = Sponsor.findAll().list(); + return Response.ok(allSubmissions).build(); + } + + @GET + @Path("{id}") + public Response sponsor(@PathParam("id") long id) { + Sponsor sponsor = Sponsor.findById(id); + return Response.ok().entity(sponsor).build(); + } + + @GET + @Path("/logo/{id}") + public Response sponsorLogo(@PathParam("id") long id) { + Sponsor sponsor = Sponsor.findById(id); + return Response.ok().type(sponsor.getType()).entity(sponsor.getLogo()).build(); + } +} diff --git a/src/main/java/bg/jug/website/user/model/User.java b/src/main/java/bg/jug/website/user/model/User.java index 4b63888..8ed6035 100644 --- a/src/main/java/bg/jug/website/user/model/User.java +++ b/src/main/java/bg/jug/website/user/model/User.java @@ -6,9 +6,7 @@ import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.Lob; -import java.util.Collections; -import java.util.List; - +import java.util.*; @Entity public class User extends AbstractEntity { @@ -30,7 +28,7 @@ public class User extends AbstractEntity { private String bio; @JsonbTransient - private String password; + private byte[] password; @JsonbTransient private String salt; @@ -42,13 +40,13 @@ public class User extends AbstractEntity { public User() { } - public User(String email, String password, String salt) { + public User(String email, byte[] password, String salt) { this(null, null, email, null, null, password, salt, - Collections.singletonList(DEFAULT_ROLE)); + new ArrayList<>(Collections.singletonList(DEFAULT_ROLE))); } public User(String nickname, String fullname, String email, byte[] photo, - String bio, String password, String salt, List roles) { + String bio, byte[] password, String salt, List roles) { this.nickname = nickname; this.fullname = fullname; this.email = email; @@ -89,10 +87,10 @@ public String getBio() { public void setBio(String bio) { this.bio = bio; } - public String getPassword() { + public byte[] getPassword() { return password; } - public void setPassword(String password) { + public void setPassword(byte[] password) { this.password = password; } public String getSalt() { diff --git a/src/main/java/bg/jug/website/user/service/UserService.java b/src/main/java/bg/jug/website/user/service/UserService.java index 9058bcd..9115e94 100644 --- a/src/main/java/bg/jug/website/user/service/UserService.java +++ b/src/main/java/bg/jug/website/user/service/UserService.java @@ -18,6 +18,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Arrays; @RequestScoped @Path("/user") @@ -33,9 +34,13 @@ public class UserService { @Transactional public Response registerUser(@Valid LoginDetails registrationDetails) { // TODO Check if email is already registered + String salt = RandomStringUtils.randomAlphanumeric(20); - String encrypted = CryptUtils.encryptPassword(registrationDetails.getPassword() + salt); + byte[] encrypted = CryptUtils.encryptPassword(registrationDetails.getPassword() + salt); User newUser = new User(registrationDetails.getEmail(), encrypted, salt); + if (User.findAll().count() == 0) { + newUser.getRoles().add("admin"); + } newUser.persist(); return Response.ok().header("Authorization", getJwt(newUser)).build(); @@ -51,8 +56,8 @@ public Response loginUser(LoginDetails loginDetails) { return Response.status(Response.Status.UNAUTHORIZED).build(); } - String encrypted = CryptUtils.encryptPassword(loginDetails.getPassword() + user.getSalt()); - if (!user.getPassword().equals(encrypted)) { + byte[] encrypted = CryptUtils.encryptPassword(loginDetails.getPassword() + user.getSalt()); + if (!Arrays.equals(user.getPassword(), encrypted)) { return Response.status(Response.Status.UNAUTHORIZED).build(); } diff --git a/src/main/resources/META-INF/resources/img/sponsors/418.png b/src/main/resources/META-INF/resources/img/sponsors/418.png new file mode 100644 index 0000000..50dfa36 Binary files /dev/null and b/src/main/resources/META-INF/resources/img/sponsors/418.png differ diff --git a/src/main/resources/META-INF/resources/img/sponsors/99.png b/src/main/resources/META-INF/resources/img/sponsors/99.png new file mode 100644 index 0000000..cb8ffe7 Binary files /dev/null and b/src/main/resources/META-INF/resources/img/sponsors/99.png differ diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html index 54f0954..9903163 100755 --- a/src/main/resources/META-INF/resources/index.html +++ b/src/main/resources/META-INF/resources/index.html @@ -38,11 +38,12 @@ - - - - - - - - - - - - + + + + + + + + + + + + + @@ -182,8 +184,8 @@
    -

    BGJUG

    -

    Bulgarian Java User Group

    +

    JProfessionals conference

    +

    Organised by Bulgarian Java User group

    @@ -281,413 +283,304 @@

    Margaret Gould

    -
    - - - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    -
    - -
    -
    Flickr
    -
    -
    - More Photos + +
    +
    Flickr
    +
    +
    + + + + + More + Photos +
    +
    -
    -
    Meta
    - -
    -->
    - - - -
    -
    -
    -
    -

    Main Sponsors

    -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -

    Platinum Sponsors

    -
    -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -

    Gold Sponsors

    -
    - -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -

    Silver Sponsors

    -
    -
    - -
    -
    - + +
    +
    +
    +
    + -
    -
    - - - -
    -
    -
    -
    -
    - - - - - - - - - - - - + + + + + + + + + + + + +