diff --git a/src/com/urise/webapp/Config.java b/src/com/urise/webapp/Config.java new file mode 100644 index 0000000..5e9465f --- /dev/null +++ b/src/com/urise/webapp/Config.java @@ -0,0 +1,37 @@ +package com.urise.webapp; + +import com.urise.webapp.storage.SqlStorage; +import com.urise.webapp.storage.Storage; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class Config { + private static final File PROPS = new File("C:/Java/BaseJava/config/resumes.properties"); + private static final Config INSTANCE = new Config(); + private final File storageDir; + + private final Storage storage; + + public static Config get() {return INSTANCE;} + + private Config() { + try(InputStream is = new FileInputStream(PROPS)) { + Properties props = new Properties(); + props.load(is); + storageDir = new File(props.getProperty("storage.dir")); + storage = new SqlStorage(props.getProperty("db.url"), + props.getProperty("db.user"), props.getProperty("db.password")); + } catch (IOException e) { + throw new IllegalStateException("Invalid config file" + PROPS.getAbsolutePath(), e); + } + } + + public File getStorageDir() {return storageDir;} + + public Storage getStorage() {return storage;} + +} diff --git a/src/com/urise/webapp/ResumeTestData.java b/src/com/urise/webapp/ResumeTestData.java index a4ff47d..a3092a5 100644 --- a/src/com/urise/webapp/ResumeTestData.java +++ b/src/com/urise/webapp/ResumeTestData.java @@ -2,12 +2,13 @@ import com.urise.webapp.model.*; -import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; public class ResumeTestData { + private ResumeTestData() {} + public static Resume createResume(String uuid, String fullName) { Resume myResume = new Resume(uuid, fullName); @@ -82,7 +83,7 @@ public static Resume createResume(String uuid, String fullName) { "архитектурных шаблонов, UML, функционального программирования"); myQualifications.add("Родной русский, английский \"upper intermediate\""); mySections.put(SectionType.QUALIFICATION, new ListSection(myQualifications)); - +/* // EXPERIENCE section List myCompanies = new ArrayList<>(); myCompanies.add(new Company("Java Online Projects", "http://javaops.ru/", @@ -149,7 +150,7 @@ public static Resume createResume(String uuid, String fullName) { for (SectionType section : SectionType.values()) { System.out.println(section.getTitle() + "\n" + myResume.getSections().get(section)); } - +*/ return myResume; } } diff --git a/src/com/urise/webapp/model/ContactType.java b/src/com/urise/webapp/model/ContactType.java index 3a04374..a503c3b 100644 --- a/src/com/urise/webapp/model/ContactType.java +++ b/src/com/urise/webapp/model/ContactType.java @@ -8,17 +8,44 @@ public enum ContactType { PHONE("Тел."), MOBILE("Мобильный"), HOME_PHONE("Домашний тел."), - SKYPE("Skype"), - MAIL("Почта"), - LINKEDIN("Профиль LinkedIn"), - GITHUB("Профиль GitHub"), - STACKOVERFLOW("Профиль Stackoverflow"), - HOME_PAGE("Домашняя страница"); + SKYPE("Skype") { + @Override + public String toHtml0(String value) { + return getTitle() + ": " + toLink("skype:" + value, value); + } + }, + MAIL("Почта") { + @Override + public String toHtml0(String value) { + return getTitle() + ": " + toLink("mailto:" + value, value); + } + }, + LINKEDIN("Профиль LinkedIn") { + @Override + public String toHtml0(String value) { + return toLink(value); + } + }, + GITHUB("Профиль GitHub") { + @Override + public String toHtml0(String value) { + return toLink(value); + } + }, + STACKOVERFLOW("Профиль Stackoverflow") { + @Override + public String toHtml0(String value) { + return toLink(value); + } + }, + HOME_PAGE("Домашняя страница") { + @Override + public String toHtml0(String value) { + return toLink(value); + } + }; - private String title; - - ContactType() { - } + private final String title; ContactType(String title) { this.title = title; @@ -27,4 +54,20 @@ public enum ContactType { public String getTitle() { return title; } + + protected String toHtml0(String value) { + return title + ": " + value; + } + + public String toHtml(String value) { + return (value == null) ? "" : toHtml0(value); + } + + public String toLink(String href) { + return toLink(href, title); + } + + public static String toLink(String href, String title) { + return "" + title + ""; + } } diff --git a/src/com/urise/webapp/model/Resume.java b/src/com/urise/webapp/model/Resume.java index 6f6d9de..3e04a87 100644 --- a/src/com/urise/webapp/model/Resume.java +++ b/src/com/urise/webapp/model/Resume.java @@ -94,4 +94,12 @@ public void addContact(ContactType contactType, String value) { public void addSections(SectionType type, Section section) { sections.put(type, section); } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getContact(ContactType contactType) { + return contacts.get(contactType); + } } diff --git a/src/com/urise/webapp/sql/ConnectionFactory.java b/src/com/urise/webapp/sql/ConnectionFactory.java new file mode 100644 index 0000000..08dff92 --- /dev/null +++ b/src/com/urise/webapp/sql/ConnectionFactory.java @@ -0,0 +1,8 @@ +package com.urise.webapp.sql; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface ConnectionFactory { + Connection getConnection() throws SQLException; +} diff --git a/src/com/urise/webapp/sql/ExceptionUtil.java b/src/com/urise/webapp/sql/ExceptionUtil.java new file mode 100644 index 0000000..e0e9651 --- /dev/null +++ b/src/com/urise/webapp/sql/ExceptionUtil.java @@ -0,0 +1,23 @@ +package com.urise.webapp.sql; + +import com.urise.webapp.exception.ExistStorageException; +import com.urise.webapp.exception.StorageException; +import org.postgresql.util.PSQLException; + +import java.sql.SQLException; + +public class ExceptionUtil { + private ExceptionUtil() { + } + + public static StorageException convertException(SQLException e) { + if (e instanceof PSQLException) { + +// http://www.postgresql.org/docs/9.3/static/errcodes-appendix.html + if (e.getSQLState().equals("23505")) { + return new ExistStorageException(null); + } + } + return new StorageException(e.getMessage()); + } +} diff --git a/src/com/urise/webapp/sql/SqlExecutor.java b/src/com/urise/webapp/sql/SqlExecutor.java new file mode 100644 index 0000000..9543bfc --- /dev/null +++ b/src/com/urise/webapp/sql/SqlExecutor.java @@ -0,0 +1,8 @@ +package com.urise.webapp.sql; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public interface SqlExecutor { + T execute(PreparedStatement st) throws SQLException; +} diff --git a/src/com/urise/webapp/sql/SqlHelper.java b/src/com/urise/webapp/sql/SqlHelper.java new file mode 100644 index 0000000..fd630ce --- /dev/null +++ b/src/com/urise/webapp/sql/SqlHelper.java @@ -0,0 +1,45 @@ +package com.urise.webapp.sql; + +import com.urise.webapp.exception.StorageException; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class SqlHelper { + private final ConnectionFactory connectionFactory; + + public SqlHelper(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + public void execute(String sql) { + execute(sql, PreparedStatement::execute); + } + + public T execute(String sql, SqlExecutor executor) { + try (Connection conn = connectionFactory.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + return executor.execute(ps); + } catch (SQLException e) { + throw ExceptionUtil.convertException(e); + } + } + + public T transactionalExecute(SqlTransaction executor) { + try (Connection conn = connectionFactory.getConnection()) { + try { + conn.setAutoCommit(false); + T res = executor.execute(conn); + conn.commit(); + return res; + } catch (SQLException e) { + conn.rollback(); + throw ExceptionUtil.convertException(e); + } + } catch (SQLException e) { + throw new StorageException(e.getMessage()); + } + } + +} diff --git a/src/com/urise/webapp/sql/SqlTransaction.java b/src/com/urise/webapp/sql/SqlTransaction.java new file mode 100644 index 0000000..c4b2485 --- /dev/null +++ b/src/com/urise/webapp/sql/SqlTransaction.java @@ -0,0 +1,9 @@ +package com.urise.webapp.sql; + +import java.sql.Connection; + +import java.sql.SQLException; + +public interface SqlTransaction { + T execute(Connection conn) throws SQLException; +} diff --git a/src/com/urise/webapp/storage/SqlStorage.java b/src/com/urise/webapp/storage/SqlStorage.java new file mode 100644 index 0000000..f014c92 --- /dev/null +++ b/src/com/urise/webapp/storage/SqlStorage.java @@ -0,0 +1,223 @@ +package com.urise.webapp.storage; + +import com.urise.webapp.exception.NotExistStorageException; +import com.urise.webapp.model.*; +import com.urise.webapp.sql.SqlHelper; +import com.urise.webapp.util.JsonParser; + +import java.sql.*; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SqlStorage implements Storage { + public final SqlHelper sqlHelper; + + public SqlStorage(String dbUrl, String dbUser, String dbPassword) { + sqlHelper = new SqlHelper(() -> DriverManager.getConnection(dbUrl, dbUser, dbPassword)); + } + + @Override + public void clear() { + sqlHelper.execute("DELETE FROM resume"); + } + + @Override + public Resume get(String uuid) { + return sqlHelper.transactionalExecute(conn -> { + Resume r; + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM resume WHERE uuid =?")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + if (!rs.next()) { + throw new NotExistStorageException(uuid); + } + r = new Resume(uuid, rs.getString("full_name")); + } + + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM contact WHERE resume_uuid =?")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + addContact(rs, r); + } + } + + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM section WHERE resume_uuid =?")) { + ps.setString(1, uuid); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + addSection(rs, r); + } + } + + return r; + }); + } + + @Override + public void update(Resume r) { + sqlHelper.transactionalExecute(conn -> { + try (PreparedStatement ps = conn.prepareStatement("UPDATE resume SET full_name = ? WHERE uuid = ?")) { + ps.setString(1, r.getFullName()); + ps.setString(2, r.getUuid()); + if (ps.executeUpdate() != 1) { + throw new NotExistStorageException(r.getUuid()); + } + } + deleteContacts(conn, r); + deleteSections(conn, r); + insertContacts(conn, r); + insertSections(conn, r); + return null; + }); + } + + @Override + public void save(Resume r) { + sqlHelper.transactionalExecute(conn -> { + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO resume (uuid, full_name) VALUES (?,?)")) { + ps.setString(1, r.getUuid()); + ps.setString(2, r.getFullName()); + ps.execute(); + } + insertContacts(conn, r); + insertSections(conn, r); + return null; + } + ); + } + + @Override + public void delete(String uuid) { + sqlHelper.execute("DELETE FROM resume WHERE uuid=?", ps -> { + ps.setString(1, uuid); + if (ps.executeUpdate() == 0) { + throw new NotExistStorageException(uuid); + } + return null; + }); + } + + @Override + public List getAllSorted() { + return sqlHelper.transactionalExecute(conn -> { + Map resumes = new LinkedHashMap<>(); + + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM resume ORDER BY full_name, uuid")) { + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + String uuid = rs.getString("uuid"); + resumes.put(uuid, new Resume(uuid, rs.getString("full_name"))); + } + } + + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM contact")) { + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Resume r = resumes.get(rs.getString("resume_uuid")); + addContact(rs, r); + } + } + + try (PreparedStatement ps = conn.prepareStatement("SELECT * FROM section")) { + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + Resume r = resumes.get(rs.getString("resume_uuid")); + addSection(rs, r); + } + } + + return new ArrayList<>(resumes.values()); + }); + } + + @Override + public int size() { + return sqlHelper.execute("SELECT count(*) FROM resume", st -> { + ResultSet rs = st.executeQuery(); + return rs.next() ? rs.getInt(1) : 0; + }); + } + + private void insertContacts(Connection conn, Resume r) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO contact (resume_uuid, type, value) VALUES (?,?,?)")) { + for (Map.Entry e : r.getContacts().entrySet()) { + ps.setString(1, r.getUuid()); + ps.setString(2, e.getKey().name()); + ps.setString(3, e.getValue()); + ps.addBatch(); + } + ps.executeBatch(); + } + } + + private void insertSections(Connection conn, Resume r) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO section (resume_uuid, type, value) VALUES (?,?,?)")) { + for (Map.Entry e : r.getSections().entrySet()) { + ps.setString(1, r.getUuid()); + ps.setString(2, e.getKey().name()); + Section section = e.getValue(); + ps.setString(3, JsonParser.write(section, Section.class)); + ps.addBatch(); + } + ps.executeBatch(); + } + } + + private void deleteContacts(Connection conn, Resume r) throws SQLException { + deleteAttributes(conn, r, "DELETE FROM contact WHERE resume_uuid=?"); + } + + private void deleteSections(Connection conn, Resume r) throws SQLException { + deleteAttributes(conn, r, "DELETE FROM section WHERE resume_uuid=?"); + } + + private void deleteAttributes(Connection conn, Resume r, String sql) throws SQLException { + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, r.getUuid()); + ps.execute(); + } + } + + private void addAllContacts(Map map) { + sqlHelper.execute("SELECT * FROM contact", + ps -> { + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + addContact(rs, map.get(rs.getString("resume_uuid"))); + } + return null; + }); + } + + private void addContacts(Resume r) { + sqlHelper.execute( + "SELECT * FROM contact c WHERE c.resume_uuid =?", + ps -> { + ps.setString(1, r.getUuid()); + ResultSet rs = ps.executeQuery(); + while (rs.next()) { + addContact(rs, r); + } + return null; + }); + } + + private void addContact(ResultSet rs, Resume r) throws SQLException { + String value = rs.getString("value"); + if (value != null) { + r.addContact(ContactType.valueOf(rs.getString("type")), value); + } + } + + private void addSection(ResultSet rs, Resume r) throws SQLException { + String content = rs.getString("value"); + if (content != null) { + SectionType type = SectionType.valueOf(rs.getString("type")); + r.addSections(type, JsonParser.read(content, Section.class)); + } + } + +} \ No newline at end of file diff --git a/src/com/urise/webapp/util/JsonParser.java b/src/com/urise/webapp/util/JsonParser.java index d6dd5a1..f908261 100644 --- a/src/com/urise/webapp/util/JsonParser.java +++ b/src/com/urise/webapp/util/JsonParser.java @@ -6,12 +6,10 @@ import java.io.Reader; import java.io.Writer; -import java.time.LocalDate; public class JsonParser { - private static final Gson GSON = new GsonBuilder() - .registerTypeAdapter(Section.class, new JsonSectionAdapter<>()) - .registerTypeAdapter(LocalDate.class, new LocalDateTypeAdapter()) + private static Gson GSON = new GsonBuilder() + .registerTypeAdapter(Section.class, new JsonSectionAdapter()) .create(); public static T read(Reader reader, Class clazz) { @@ -21,4 +19,16 @@ public static T read(Reader reader, Class clazz) { public static void write(T object, Writer writer) { GSON.toJson(object, writer); } -} + + public static T read(String content, Class clazz) { + return GSON.fromJson(content, clazz); + } + + public static String write(T object) { + return GSON.toJson(object); + } + + public static String write(T object, Class clazz) { + return GSON.toJson(object, clazz); + } +} \ No newline at end of file diff --git a/src/com/urise/webapp/util/JsonSectionAdapter.java b/src/com/urise/webapp/util/JsonSectionAdapter.java index 13ddbd7..2113163 100644 --- a/src/com/urise/webapp/util/JsonSectionAdapter.java +++ b/src/com/urise/webapp/util/JsonSectionAdapter.java @@ -22,6 +22,7 @@ public T deserialize(JsonElement json, Type type, JsonDeserializationContext con } } + @Override public JsonElement serialize(T section, Type type, JsonSerializationContext context) { JsonObject retValue = new JsonObject(); diff --git a/src/com/urise/webapp/web/ResumeServlet.java b/src/com/urise/webapp/web/ResumeServlet.java new file mode 100644 index 0000000..ad6e6c6 --- /dev/null +++ b/src/com/urise/webapp/web/ResumeServlet.java @@ -0,0 +1,69 @@ +package com.urise.webapp.web; + +import com.urise.webapp.Config; +import com.urise.webapp.model.ContactType; +import com.urise.webapp.model.Resume; +import com.urise.webapp.storage.Storage; +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class ResumeServlet extends HttpServlet { + + private Storage storage; // = Config.get().getStorage(); + + @Override + public void init(ServletConfig config) throws ServletException { + super.init(config); + storage = Config.get().getStorage(); + } + + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + request.setCharacterEncoding("UTF-8"); + String uuid = request.getParameter("uuid"); + String fullName = request.getParameter("fullName"); + Resume r = storage.get(uuid); + r.setFullName(fullName); + for (ContactType type : ContactType.values()) { + String value = request.getParameter(type.name()); + if (value != null && value.trim().length() != 0) { + r.addContact(type, value); + } else { + r.getContacts().remove(type); + } + } + storage.update(r); + response.sendRedirect("resume"); + } + + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String uuid = request.getParameter("uuid"); + String action = request.getParameter("action"); + if (action == null) { + request.setAttribute("resumes", storage.getAllSorted()); + request.getRequestDispatcher("/WEB-INF/jsp/list.jsp").forward(request, response); + return; + } + Resume r; + switch (action) { + case "delete": + storage.delete(uuid); + response.sendRedirect("resume"); + return; + case "view": + case "edit": + r = storage.get(uuid); + break; + default: + throw new IllegalArgumentException("Action " + action + " is illegal"); + } + request.setAttribute("resume", r); + request.getRequestDispatcher( + ("view".equals(action) ? "/WEB-INF/jsp/view.jsp" : "/WEB-INF/jsp/edit.jsp") + ).forward(request, response); + } +} \ No newline at end of file diff --git a/test/com/urise/webapp/storage/AbstractStorageTest.java b/test/com/urise/webapp/storage/AbstractStorageTest.java index 90adaf5..0bda946 100644 --- a/test/com/urise/webapp/storage/AbstractStorageTest.java +++ b/test/com/urise/webapp/storage/AbstractStorageTest.java @@ -1,27 +1,31 @@ package com.urise.webapp.storage; +import com.urise.webapp.Config; import com.urise.webapp.exception.ExistStorageException; import com.urise.webapp.exception.NotExistStorageException; +import com.urise.webapp.model.ContactType; import com.urise.webapp.model.Resume; import org.junit.Before; import org.junit.Test; import java.io.File; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.UUID; import static com.urise.webapp.ResumeTestData.createResume; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public abstract class AbstractStorageTest { - protected static final File STORAGE_DIR = new File("basejava/storage"); + protected static final File STORAGE_DIR = Config.get().getStorageDir(); protected final Storage storage; - private static final String UUID_1 = "uuid1"; - private static final String UUID_2 = "uuid2"; - private static final String UUID_3 = "uuid3"; - private static final String UUID_4 = "uuid4"; + private static final String UUID_1 = UUID.randomUUID().toString(); + private static final String UUID_2 = UUID.randomUUID().toString(); + private static final String UUID_3 = UUID.randomUUID().toString(); + private static final String UUID_4 = UUID.randomUUID().toString(); private static final Resume RESUME_1; private static final Resume RESUME_2; @@ -61,6 +65,8 @@ public void clear() throws Exception { @Test public void update() throws Exception { Resume newResume = createResume(UUID_1, "newFirstResume"); + newResume.addContact(ContactType.HOME_PAGE, "http://gkislin.ru/"); + newResume.getContacts().put(ContactType.MAIL, "edarg@wsfws.ru"); storage.update(newResume); Resume oldRes = storage.get(UUID_1); assertEquals(newResume, oldRes); @@ -75,7 +81,9 @@ public void updateNotExist() throws Exception { public void getAllSorted() throws Exception { List list = storage.getAllSorted(); assertEquals(3, list.size()); - assertEquals(list, Arrays.asList(RESUME_1, RESUME_2, RESUME_3)); + List sortedList = Arrays.asList(RESUME_1, RESUME_2, RESUME_3); + Collections.sort(sortedList); + assertEquals(sortedList, list); } @Test diff --git a/test/com/urise/webapp/storage/AllStorageTest.java b/test/com/urise/webapp/storage/AllStorageTest.java index 31f6a43..c88d10b 100644 --- a/test/com/urise/webapp/storage/AllStorageTest.java +++ b/test/com/urise/webapp/storage/AllStorageTest.java @@ -14,7 +14,8 @@ PathStorageTest.class, XmlPathStorageTest.class, JsonPathStorageTest.class, - DataPathStorageTest.class + DataPathStorageTest.class, + SqlStorageTest.class }) public class AllStorageTest { } diff --git a/test/com/urise/webapp/storage/SqlStorageTest.java b/test/com/urise/webapp/storage/SqlStorageTest.java new file mode 100644 index 0000000..afc9da9 --- /dev/null +++ b/test/com/urise/webapp/storage/SqlStorageTest.java @@ -0,0 +1,9 @@ +package com.urise.webapp.storage; + +import com.urise.webapp.Config; + +public class SqlStorageTest extends AbstractStorageTest { + public SqlStorageTest() { + super(Config.get().getStorage()); + } +} \ No newline at end of file