From ebe41c50ffb6356d20d76fe32b034851cafe3401 Mon Sep 17 00:00:00 2001 From: John Grib Date: Thu, 20 Apr 2017 20:19:27 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BB=A4=EC=8A=A4=ED=85=80=20JDBC=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EB=A7=8C=EB=93=A4?= =?UTF-8?q?=EC=96=B4=20=EB=B3=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/core/db/Query.java | 96 +++++++++ src/main/java/core/db/ResultData.java | 88 ++++++++ src/main/java/core/jdbc/JdbcTemplate.java | 134 +++++++++++++ src/main/java/next/dao/UserDao.java | 67 +------ src/main/java/next/model/User.java | 23 +++ src/main/java/util/DataMethod.java | 53 +++++ src/main/java/util/ReflectionUtil.java | 222 +++++++++++++++++++++ src/main/java/util/RegexUtil.java | 57 ++++++ src/main/java/util/UpperStringMap.java | 25 +++ src/test/java/util/ReflectionUtilTest.java | 58 ++++++ src/test/java/util/RegexUtilTest.java | 31 +++ 11 files changed, 798 insertions(+), 56 deletions(-) create mode 100644 src/main/java/core/db/Query.java create mode 100644 src/main/java/core/db/ResultData.java create mode 100644 src/main/java/core/jdbc/JdbcTemplate.java create mode 100644 src/main/java/util/DataMethod.java create mode 100644 src/main/java/util/ReflectionUtil.java create mode 100644 src/main/java/util/RegexUtil.java create mode 100644 src/main/java/util/UpperStringMap.java create mode 100644 src/test/java/util/ReflectionUtilTest.java create mode 100644 src/test/java/util/RegexUtilTest.java diff --git a/src/main/java/core/db/Query.java b/src/main/java/core/db/Query.java new file mode 100644 index 000000000..8cbbff00e --- /dev/null +++ b/src/main/java/core/db/Query.java @@ -0,0 +1,96 @@ +package core.db; + +import util.ReflectionUtil; +import util.UpperStringMap; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Query String 을 생성/관리한다. + * Created by johngrib on 2017. 4. 22.. + */ +public class Query { + + private static List processors = Arrays.asList(new ValueProc[]{ + new NullProc(), + new StringProc(), + new IntegerProc() + }); + + private static String buildAlias(final String key) { + return "(?i)" + Pattern.quote("${" + key + "}"); + } + + /** + * query 에 vo 의 값을 set 한다. + * query 에서 replace 될 키는 ${key} 의 형태로 지정한다. + * + * @param sql + * @param vo + * @return + */ + public static String build(final String sql, final Object vo) { + if (vo == null) { + return sql; + } + + final Map map = ReflectionUtil.objMapper(vo, new UpperStringMap()); + + String sourceSql = sql; + + for (final String key : map.keySet()) { + final Object val = map.get(key); + final String value = processors.stream() + .filter(p -> p.typeCheck(val)) + .findFirst().get().proc(val); + sourceSql = sourceSql.replaceAll(buildAlias(key), value); + } + return sourceSql; + } + + abstract static class ValueProc { + abstract String proc(Object val); + + abstract boolean typeCheck(Object val); + } + + static class StringProc extends ValueProc { + @Override + public String proc(final Object val) { + return "'" + String.valueOf(val) + "'"; + } + + @Override + boolean typeCheck(final Object val) { + return val instanceof String; + } + } + + static class NullProc extends ValueProc { + @Override + public String proc(final Object val) { + return "'" + String.valueOf(val) + "'"; + } + + @Override + boolean typeCheck(final Object val) { + return val == null; + } + } + + static class IntegerProc extends ValueProc { + + @Override + String proc(final Object val) { + return String.valueOf(val); + } + + @Override + boolean typeCheck(final Object val) { + return val instanceof Integer; + } + } +} diff --git a/src/main/java/core/db/ResultData.java b/src/main/java/core/db/ResultData.java new file mode 100644 index 000000000..2a7f023fa --- /dev/null +++ b/src/main/java/core/db/ResultData.java @@ -0,0 +1,88 @@ +package core.db; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.*; + +/** + * ResultSet Wrapper + * Created by johngrib on 2017. 4. 22.. + */ +public class ResultData { + + final private List labels; + private ResultSet rs = null; + + public ResultData(final ResultSet rs) throws SQLException { + this.rs = rs; + this.labels = getColumnLabels(rs); + } + + /** + * column labels를 리턴한다. + * @return + */ + public List getLabels() { + return labels; + } + + /** + * column lable 을 수집한다. + * @param rs + * @return + */ + private List getColumnLabels(final ResultSet rs) { + try { + final int size = rs.getMetaData().getColumnCount(); + final List list = new ArrayList<>(size); + final ResultSetMetaData meta = rs.getMetaData(); + + for (int i = 1; i <= size; i++) { + String name = meta.getColumnName(i); + list.add(name); + } + return list; + } catch (SQLException e) { + e.printStackTrace(); + } + return Collections.EMPTY_LIST; + } + + public ResultSet getResultSet() { + return rs; + } + + public void close() { + try { + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } finally { + rs = null; + } + } + + public boolean next() { + try { + return rs.next(); + } catch (SQLException e) { + e.printStackTrace(); + } + return false; + } + + public Map getDataMap() { + try { + final Map data = new HashMap<>(); + for (String label : labels) { + data.put(label, rs.getObject(label)); + } + return data; + + } catch (SQLException e) { + e.printStackTrace(); + } + return Collections.EMPTY_MAP; + } +} diff --git a/src/main/java/core/jdbc/JdbcTemplate.java b/src/main/java/core/jdbc/JdbcTemplate.java new file mode 100644 index 000000000..2eb2009aa --- /dev/null +++ b/src/main/java/core/jdbc/JdbcTemplate.java @@ -0,0 +1,134 @@ +package core.jdbc; + +import core.db.Query; +import core.db.ResultData; +import util.DataMethod; +import util.ReflectionUtil; +import util.UpperStringMap; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +/** + * Created by johngrib on 2017. 4. 20.. + */ +public class JdbcTemplate { + + /** + * INSERT, UPDATE 를 수행한다. + * + * @param sql + * @param vo + * @throws SQLException + */ + public void update(String sql, Object vo) throws SQLException { + Connection con = null; + PreparedStatement pstmt = null; + try { + con = ConnectionManager.getConnection(); + pstmt = con.prepareStatement(Query.build(sql, vo)); + pstmt.executeUpdate(); + } finally { + if (pstmt != null) { + pstmt.close(); + } + + if (con != null) { + con.close(); + } + } + } + + /** + * SELECT 를 수행한다. + * @param sql + * @param voClass + * @return + * @throws SQLException + */ + public List select(String sql, Class voClass) throws SQLException { + return select(sql, voClass, null); + } + + /** + * SELECT 를 수행한 결과의 첫 번째 row 를 리턴한다. + * @param sql + * @param voClass + * @param vo + * @return + * @throws SQLException + */ + public T selectOne(String sql, Class voClass, Object vo) throws SQLException { + final List list = select(sql, voClass, vo); + return (list.size() > 0) ? list.get(0) : null; + } + + /** + * SELECT 를 수행한다. + * + * @param sql + * @param voClass + * @return + * @throws SQLException + */ + public List select(String sql, Class voClass, Object vo) throws SQLException { + + ResultSet rs = null; + final String query = Query.build(sql, vo); + try ( + final Connection con = ConnectionManager.getConnection(); + final PreparedStatement pstmt = con.prepareStatement(query); + ) { + rs = pstmt.executeQuery(); + + final ResultData rd = new ResultData(rs); + final List list = new ArrayList<>(rs.getFetchSize()); + final Map setters = ReflectionUtil.getSetterMemberMap(voClass, new UpperStringMap()); + final List labels = rd.getLabels(); + + while (rd.next()) { + + final T row = ReflectionUtil.newSimpleInstance(voClass); + + for (String label: labels) { + DataMethod setter = setters.get(label); + setVoFromResultSet(setter, rd.getResultSet(), row); + } + list.add(row); + } + return list; + } finally { + if (rs != null) { + rs.close(); + } + } + } + + /** + * ResultSet 의 데이터를 vo 에 매핑한다. + * @param m + * @param rs + * @param vo + */ + private void setVoFromResultSet(final DataMethod m, final ResultSet rs, Object vo) { + try { + if (String.class.equals(m.type)) { + m.setter(vo, rs.getString(m.fieldName)); + return; + } + if (Integer.class.equals(m.type)) { + m.setter(vo, rs.getInt(m.fieldName)); + return; + } + if (Double.class.equals(m.type)) { + m.setter(vo, rs.getInt(m.fieldName)); + return; + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/next/dao/UserDao.java b/src/main/java/next/dao/UserDao.java index db270861f..a756b6db1 100644 --- a/src/main/java/next/dao/UserDao.java +++ b/src/main/java/next/dao/UserDao.java @@ -1,78 +1,33 @@ package next.dao; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; import java.sql.SQLException; -import java.util.ArrayList; import java.util.List; -import core.jdbc.ConnectionManager; +import core.jdbc.JdbcTemplate; import next.model.User; public class UserDao { - public void insert(User user) throws SQLException { - Connection con = null; - PreparedStatement pstmt = null; - try { - con = ConnectionManager.getConnection(); - String sql = "INSERT INTO USERS VALUES (?, ?, ?, ?)"; - pstmt = con.prepareStatement(sql); - pstmt.setString(1, user.getUserId()); - pstmt.setString(2, user.getPassword()); - pstmt.setString(3, user.getName()); - pstmt.setString(4, user.getEmail()); - pstmt.executeUpdate(); - } finally { - if (pstmt != null) { - pstmt.close(); - } + public void insert(User user) throws SQLException { - if (con != null) { - con.close(); - } - } + final String sql = "INSERT INTO USERS VALUES (${userId}, ${password}, ${name}, ${email})"; + new JdbcTemplate().update(sql, user); } public void update(User user) throws SQLException { - // TODO 구현 필요함. + final String sql = "UPDATE USERS SET password = ${password}, name = ${name}, email = ${email} WHERE userId = ${userId}"; + new JdbcTemplate().update(sql, user); } public List findAll() throws SQLException { - // TODO 구현 필요함. - return new ArrayList(); + String sql = "SELECT userid, password, name, email FROM USERS"; + return new JdbcTemplate().select(sql, User.class); } public User findByUserId(String userId) throws SQLException { - Connection con = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - try { - con = ConnectionManager.getConnection(); - String sql = "SELECT userId, password, name, email FROM USERS WHERE userid=?"; - pstmt = con.prepareStatement(sql); - pstmt.setString(1, userId); - - rs = pstmt.executeQuery(); - - User user = null; - if (rs.next()) { - user = new User(rs.getString("userId"), rs.getString("password"), rs.getString("name"), - rs.getString("email")); - } + String sql = "SELECT userId, password, name, email FROM USERS WHERE userid=${userId}"; + User user = new User(userId, null, null, null); - return user; - } finally { - if (rs != null) { - rs.close(); - } - if (pstmt != null) { - pstmt.close(); - } - if (con != null) { - con.close(); - } - } + return (User) new JdbcTemplate().selectOne(sql, User.class, user); } } diff --git a/src/main/java/next/model/User.java b/src/main/java/next/model/User.java index b0bfef294..d4e07f0be 100644 --- a/src/main/java/next/model/User.java +++ b/src/main/java/next/model/User.java @@ -5,6 +5,9 @@ public class User { private String password; private String name; private String email; + private Integer age; + + public User() {} public User(String userId, String password, String name, String email) { this.userId = userId; @@ -13,21 +16,41 @@ public User(String userId, String password, String name, String email) { this.email = email; } + public Integer getAge() { + return this.age; + } + public void setAge(Integer age) { + this.age = age; + } + public String getUserId() { return userId; } + public void setUserId(String userId) { + this.userId = userId; + } + public String getPassword() { return password; } + public void setPassword(String password) { + this.password = password; + } public String getName() { return name; } + public void setName(String name) { + this.name = name; + } public String getEmail() { return email; } + public void setEmail(String email) { + this.email = email; + } public void update(User updateUser) { this.password = updateUser.password; diff --git a/src/main/java/util/DataMethod.java b/src/main/java/util/DataMethod.java new file mode 100644 index 000000000..1706d8c94 --- /dev/null +++ b/src/main/java/util/DataMethod.java @@ -0,0 +1,53 @@ +package util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Getter, Setter Method Wrapper 로 사용하기 위해 만든 클래스. + * Created by johngrib on 2017. 4. 22.. + */ +public class DataMethod { + + final public Class type; + final public String fieldName; + final public Method method; + + public DataMethod(Class type, String field, Method method) { + this.type = type; + this.fieldName = field.toUpperCase(); + this.method = method; + } + + public boolean isGetter() { + return method.getReturnType() != null; + } + + public boolean isSetter() { + return method.getName().startsWith("set") + && method.getParameterCount() == 1 + && !isGetter() + ; + } + + public Object getter(final Object invoker) { + try { + return method.invoke(invoker, new Object[]{}); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + public void setter(final Object invoker, final Object value) { + try { + method.invoke(invoker, value); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/util/ReflectionUtil.java b/src/main/java/util/ReflectionUtil.java new file mode 100644 index 000000000..9569a406b --- /dev/null +++ b/src/main/java/util/ReflectionUtil.java @@ -0,0 +1,222 @@ +package util; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class ReflectionUtil { + + static final String CLASS_REGEX = "^(.+)\\.class$"; + static final String DOT = "."; + static final String SLASH = "/"; + + /** + * package 안에 정의된 모든 class 를 수집한다. + */ + public static List> getClassesInPackage(final String packageName) { + final String address = packageName.replace(DOT, SLASH); + final URL scannedUrl = Thread.currentThread().getContextClassLoader().getResource(address); + + if (scannedUrl == null) { + throw new IllegalArgumentException(packageName); + } + + final File[] files = new File(scannedUrl.getFile()).listFiles(); + final List> classes = new ArrayList<>(); + + for (final File file : files) { + classes.addAll(find(file, packageName)); + } + + return classes; + } + + /** + * package 안에 정의된 모든 class 를 수집하여 name, class 의 Map 으로 만든다. + * + * @param packageName + * @return + */ + public static Map> collectClassesInPackage(final String packageName) { + return getClassesInPackage(packageName) + .stream() + .collect( + Collectors.toMap(Class::getSimpleName, Function.identity()) + ); + } + + /** + * 어노테이션이 붙은 클래스를 수집한다. + * + * @param classes + * @param anno + * @return + */ + public static List> getAnnotatedClasses(List> classes, Class anno) { + List> rs = new ArrayList<>(); + + for (Class c : classes) { + if (c.isAnnotationPresent(anno)) { + rs.add(c); + } + } + + return rs; + } + + /** + * class 파일을 검색한다. [재귀] 사용. + * + * @param file + * @param packageName + * @return + */ + private static List> find(final File file, final String packageName) { + + final List> classes = new ArrayList>(); + + final String resource = packageName + DOT + file.getName(); + + if (file.isDirectory()) { + Arrays.stream(file.listFiles()).forEach( + subFile -> classes.addAll(find(subFile, resource)) // 재귀 + ); + return classes; + } + + if (!resource.matches(CLASS_REGEX)) { + return classes; + } + + final String className = RegexUtil.exec(CLASS_REGEX, resource).get(1); + + try { + classes.add(Class.forName(className)); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return classes; + } + + /** + * getter 메소드를 사용하여 map 을 리턴한다. + * + * @param obj + * @return + */ + public static Map objMapper(final Object obj, final Map map) { + + final Map getters = getGetterMemberMap(obj.getClass()); + + for (final String key : getters.keySet()) { + final Object val = getters.get(key).getter(obj); + map.put(key, val); + } + return map; + } + + /** + * getter 메소드의 콜렉션을 리턴한다. + * + * @param c + * @param + * @return + */ + public static Map getGetterMemberMap(final Class c) { + + final Map map = new UpperStringMap(); + + Arrays.stream(c.getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 0) + .filter(m -> m.getName().startsWith("get")) + .forEach(m -> { + final Class type = m.getReturnType(); + final String keyName = m.getName().replaceFirst("^get", ""); + final String key = keyName.substring(0, 1).toLowerCase() + keyName.substring(1); + map.put(key, new DataMethod(type, key, m)); + }); + return map; + } + + /** + * setter 메소드의 콜렉션을 리턴한다. + * + * @param c + * @param + * @return + */ + public static Map getSetterMemberMap(final Class c, final Map map) { + + Arrays.stream(c.getDeclaredMethods()) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> m.getName().startsWith("set")) + .forEach(m -> { + final String keyName = m.getName().replaceFirst("^set", ""); + final String key = keyName.substring(0, 1).toLowerCase() + keyName.substring(1); + final Class type = m.getParameterTypes()[0]; + map.put(key, new DataMethod(type, key, m)); + }); + return map; + } + + /** + * setter 메소드의 콜렉션을 리턴한다. + * @param c + * @param + * @return + */ + public static List getSetterList(final Class c) { + Map map = getSetterMemberMap(c, new UpperStringMap()); + return map.keySet() + .stream() + .map(key -> map.get(key)).collect(Collectors.toList()); + } + + /** + * T 클래스의 기본 생성자를 호출하여 인스턴스를 생성한다. + * + * @param c + * @param + * @return + */ + public static T newSimpleInstance(final Class c) { + try { + final Constructor con = c.getConstructor(new Class[]{}); + return (T) con.newInstance(new Object[]{}); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InstantiationException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return null; + } + + /** + * VO 객체의 새로운 인스턴스를 생성하고 ^set 메서드를 통해 map 의 값을 set 한다. + * + * @param voClass + * @param map + * @param + * @return + */ + public static VO createVo(final Class voClass, final Map map) { + + final VO rs = newSimpleInstance(voClass); + final Map setters = getSetterMemberMap(voClass, new UpperStringMap()); + + for (final String key : setters.keySet()) { + final DataMethod m = setters.get(key); + m.setter(rs, map.get(key)); + } + return rs; + } + +} diff --git a/src/main/java/util/RegexUtil.java b/src/main/java/util/RegexUtil.java new file mode 100644 index 000000000..aa4b3162e --- /dev/null +++ b/src/main/java/util/RegexUtil.java @@ -0,0 +1,57 @@ +package util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static java.util.Collections.EMPTY_LIST; + +public class RegexUtil { + + /** + * javascript 의 exec 처럼 작동하는 메서드 + */ + static public ExecResult exec(final String regex, final String text) { + final List result = new ArrayList<>(); + final Matcher m = Pattern.compile(regex).matcher(text); + + if(!m.find()) { + return new ExecResult(result); + } + + final int cnt = m.groupCount(); + for (int i = 0; i <= cnt; i++) { + result.add(m.group(i)); + } + return new ExecResult(result); + } + + static public class ExecResult { + + List result = EMPTY_LIST; + + public ExecResult(final List result) { + this.result = result; + } + + public String get(final int capture) { + if(result.isEmpty() || result.size() <= capture) { + return null; + } + return result.get(capture); + } + + public int size() { + return result.size(); + } + + public List list() { + final List newList = new ArrayList<>(result.size()); + newList.addAll(result); + return Collections.unmodifiableList(newList); + } + } +} + diff --git a/src/main/java/util/UpperStringMap.java b/src/main/java/util/UpperStringMap.java new file mode 100644 index 000000000..885814254 --- /dev/null +++ b/src/main/java/util/UpperStringMap.java @@ -0,0 +1,25 @@ +package util; + +import java.util.HashMap; + +/** + * key 를 대문자로만 취급하는 HashMap. ResultSet 의 ColumnName 이 대문자만 취급해서 만들었다. + * Created by johngrib on 2017. 4. 22.. + */ +public class UpperStringMap extends HashMap { + + @Override + public Object get(Object key) { + return super.get(key.toString().toUpperCase()); + } + + @Override + public Object put(Object key, Object value) { + return super.put(key.toString().toUpperCase(), value); + } + + @Override + public boolean containsKey(Object key) { + return super.containsKey(key.toString().toUpperCase()); + } +} diff --git a/src/test/java/util/ReflectionUtilTest.java b/src/test/java/util/ReflectionUtilTest.java new file mode 100644 index 000000000..8056c4d77 --- /dev/null +++ b/src/test/java/util/ReflectionUtilTest.java @@ -0,0 +1,58 @@ +package util; + +import next.model.User; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; + +/** + * Created by johngrib on 2017. 4. 22.. + */ +public class ReflectionUtilTest { + + @Test + public void objMapper_테스트() { + + final User user = new User("id", "password", "name", null); + user.setAge(11); + + final Map map = ReflectionUtil.objMapper(user, new UpperStringMap()); + + assertThat(user.getUserId(), is(map.get("userId"))); + assertThat(user.getPassword(), is(map.get("password"))); + assertThat(user.getName(), is(map.get("name"))); + assertThat(user.getAge(), is(map.get("age"))); + } + + @Test + public void createVo_테스트() { + + final User user = new User("id", "password", "name", null); + user.setAge(11); + + final Map map = new UpperStringMap() {{ + put("userId", "id"); + put("password", "password"); + put("name", "name"); + put("age", 11); + }}; + + User user2 = ReflectionUtil.createVo(User.class, map); + assertEquals(user, user2); + } + + @Test + public void getMemberList_테스트() { + + Map getters = ReflectionUtil.getGetterMemberMap(User.class); + + assertTrue(getters.containsKey("userId")); + assertTrue(getters.containsKey("name")); + assertTrue(getters.containsKey("password")); + assertTrue(getters.containsKey("email")); + } +} \ No newline at end of file diff --git a/src/test/java/util/RegexUtilTest.java b/src/test/java/util/RegexUtilTest.java new file mode 100644 index 000000000..019bf41bd --- /dev/null +++ b/src/test/java/util/RegexUtilTest.java @@ -0,0 +1,31 @@ +package util; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.*; +import static org.hamcrest.CoreMatchers.*; + +/** + * Created by johngrib on 2017. 4. 22.. + */ +public class RegexUtilTest { + + + @Test + public void test_exec() { + final String regex = "^(.+)\\/(.+)\\.(class)$"; + final String text = "src/test.class"; + + final List exec = RegexUtil.exec(regex, text).list(); + final List expect = new ArrayList<>(); + expect.add(text); + expect.add("src"); + expect.add("test"); + expect.add("class"); + + assertThat(expect, is(exec)); + } +} \ No newline at end of file