Apache Commons IO — это библиотека Java, созданная и поддерживаемая Apache Foundation . Он предоставляет множество классов, которые позволяют разработчикам выполнять общие задачи легко и с гораздо меньшим количеством стандартного кода , который нужно писать снова и снова для каждого отдельного проекта. Важность таких библиотек огромна, потому что они зрелые и поддерживается опытными разработчиками , которые продумали каждый возможный крайний случай или исправили различные ошибки, когда они появились.
В этом примере мы собираемся представить некоторые методы с различной функциональностью, в зависимости от пакета org.apache.commons.io
которому они принадлежат. Мы не собираемся углубляться в библиотеку, так как она огромна, но мы собираемся предоставить примеры для некоторого общего использования, которое определенно может пригодиться каждому разработчику, начинающему или нет.
1. Пример Apache Commons IO
Код для этого примера будет разбит на несколько классов, и каждый из них будет представлять определенную область, которую охватывает Apache Commons IO . Эти области:
- Утилиты
- вход
- Выход
- фильтры
- Компараторы
- Файловый монитор
Чтобы сделать вещи еще яснее, мы собираемся разбить вывод на куски , по одному для каждого из классов, которые мы создали. Мы также создали каталог внутри папки проекта (с именем ExampleFolder ), который будет содержать различные файлы, которые будут использоваться в этом примере для демонстрации функциональности различных классов.
ПРИМЕЧАНИЕ. Чтобы использовать org.apache.commons.io
, необходимо загрузить файлы jar (см. Здесь ) и добавить их в путь сборки вашего проекта Eclipse, щелкнув правой кнопкой мыши папку проекта -> Путь сборки -> Добавить внешние архивы.
ApacheCommonsExampleMain.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public class ApacheCommonsExampleMain { public static void main(String[] args) { UtilityExample.runExample(); FileMonitorExample.runExample(); FiltersExample.runExample(); InputExample.runExample(); OutputExample.runExample(); ComparatorExample.runExample(); } } |
Это основной класс, который будет использоваться для запуска методов из других классов нашего примера. Вы можете комментировать определенные классы, чтобы увидеть результат, который вы хотите.
1.1 Сервисные классы
Внутри пакета org.apache.commons.io
есть различные классы Utility, большинство из которых связано с манипулированием файлами и сравнением строк. Мы использовали некоторые из наиболее важных здесь:
-
FilenameUtils
: у этого класса есть методы, которые работают с именами файлов , и главное состоит в том, чтобы облегчить жизнь в каждой ОС (одинаково хорошо работает в системах Unix и Windows). -
FileUtils
: предоставляет методы для манипулирования файлами (перемещение, открытие и чтение файла, проверка существования файла и т. Д.). -
IOCase
: методы работы соIOCase
и сравнения . -
FileSystemUtils
: его методы возвращают свободное место на указанном диске.
UtilityExample.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
import java.io.File; import java.io.IOException; import org.apache.commons.io.FileSystemUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.LineIterator; import org.apache.commons.io.IOCase; public final class UtilityExample { // We are using the file exampleTxt.txt in the folder ExampleFolder, // and we need to provide the full path to the Utility classes. private static final String EXAMPLE_TXT_PATH = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\exampleTxt.txt" ; private static final String PARENT_DIR = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample" ; public static void runExample() throws IOException { System.out.println( "Utility Classes example..." ); // FilenameUtils System.out.println( "Full path of exampleTxt: " + FilenameUtils.getFullPath(EXAMPLE_TXT_PATH)); System.out.println( "Full name of exampleTxt: " + FilenameUtils.getName(EXAMPLE_TXT_PATH)); System.out.println( "Extension of exampleTxt: " + FilenameUtils.getExtension(EXAMPLE_TXT_PATH)); System.out.println( "Base name of exampleTxt: " + FilenameUtils.getBaseName(EXAMPLE_TXT_PATH)); // FileUtils // We can create a new File object using FileUtils.getFile(String) // and then use this object to get information from the file. File exampleFile = FileUtils.getFile(EXAMPLE_TXT_PATH); LineIterator iter = FileUtils.lineIterator(exampleFile); System.out.println( "Contents of exampleTxt..." ); while (iter.hasNext()) { System.out.println( "\t" + iter.next()); } iter.close(); // We can check if a file exists somewhere inside a certain directory. File parent = FileUtils.getFile(PARENT_DIR); System.out.println( "Parent directory contains exampleTxt file: " + FileUtils.directoryContains(parent, exampleFile)); // IOCase String str1 = "This is a new String." ; String str2 = "This is another new String, yes!" ; System.out.println( "Ends with string (case sensitive): " + IOCase.SENSITIVE.checkEndsWith(str1, "string." )); System.out.println( "Ends with string (case insensitive): " + IOCase.INSENSITIVE.checkEndsWith(str1, "string." )); System.out.println( "String equality: " + IOCase.SENSITIVE.checkEquals(str1, str2)); // FileSystemUtils System.out.println( "Free disk space (in KB): " + FileSystemUtils.freeSpaceKb( "C:" )); System.out.println( "Free disk space (in MB): " + FileSystemUtils.freeSpaceKb( "C:" ) / 1024 ); } } |
Выход
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Utility Classes example... Full path of exampleTxt: C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\ Full name of exampleTxt: exampleTxt.txt Extension of exampleTxt: txt Base name of exampleTxt: exampleTxt Contents of exampleTxt... This is an example text file . We will use it for experimenting with Apache Commons IO. Parent directory contains exampleTxt file : true Ends with string ( case sensitive): false Ends with string ( case insensitive): true String equality: false Free disk space ( in KB): 32149292 Free disk space ( in MB): 31395 |
1.2 Файловый монитор
Пакет org.apache.commons.io.monitor
содержит методы, которые могут получить конкретную информацию о файле, но, что более важно, он может создавать обработчики, которые можно использовать для отслеживания изменений в определенном файле или папке и выполнения действий в зависимости от изменений , Давайте посмотрим на код:
FileMonitorExample.java
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
|
import java.io.File; import java.io.IOException; import org.apache.commons.io.FileDeleteStrategy; import org.apache.commons.io.FileUtils; import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; import org.apache.commons.io.monitor.FileAlterationMonitor; import org.apache.commons.io.monitor.FileAlterationObserver; import org.apache.commons.io.monitor.FileEntry; public final class FileMonitorExample { private static final String EXAMPLE_PATH = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\exampleFileEntry.txt" ; private static final String PARENT_DIR = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder" ; private static final String NEW_DIR = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\newDir" ; private static final String NEW_FILE = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\newFile.txt" ; public static void runExample() { System.out.println( "File Monitor example..." ); // FileEntry // We can monitor changes and get information about files // using the methods of this class. FileEntry entry = new FileEntry(FileUtils.getFile(EXAMPLE_PATH)); System.out.println( "File monitored: " + entry.getFile()); System.out.println( "File name: " + entry.getName()); System.out.println( "Is the file a directory?: " + entry.isDirectory()); // File Monitoring // Create a new observer for the folder and add a listener // that will handle the events in a specific directory and take action. File parentDir = FileUtils.getFile(PARENT_DIR); FileAlterationObserver observer = new FileAlterationObserver(parentDir); observer.addListener( new FileAlterationListenerAdaptor() { @Override public void onFileCreate(File file) { System.out.println( "File created: " + file.getName()); } @Override public void onFileDelete(File file) { System.out.println( "File deleted: " + file.getName()); } @Override public void onDirectoryCreate(File dir) { System.out.println( "Directory created: " + dir.getName()); } @Override public void onDirectoryDelete(File dir) { System.out.println( "Directory deleted: " + dir.getName()); } }); // Add a monior that will check for events every x ms, // and attach all the different observers that we want. FileAlterationMonitor monitor = new FileAlterationMonitor( 500 , observer); try { monitor.start(); // After we attached the monitor, we can create some files and directories // and see what happens! File newDir = new File(NEW_DIR); File newFile = new File(NEW_FILE); newDir.mkdirs(); newFile.createNewFile(); Thread.sleep( 1000 ); FileDeleteStrategy.NORMAL.delete(newDir); FileDeleteStrategy.NORMAL.delete(newFile); Thread.sleep( 1000 ); monitor.stop(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } } |
Выход
1
2
3
4
5
6
7
8
|
File Monitor example... File monitored: C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\exampleFileEntry.txt File name: exampleFileEntry.txt Is the file a directory?: false Directory created: newDir File created: newFile.txt Directory deleted: newDir File deleted: newFile.txt |
Давайте посмотрим, что здесь произошло. Мы использовали некоторые классы пакета org.apache.commons.io.monitor
, которые позволяют нам создавать обработчики, которые прослушивают определенные события (в нашем случае все, что связано с файлами, папками, каталогами и т. Д.). Для этого необходимо предпринять определенные шаги:
- Создайте объект
File
, который является ссылкой на каталог, который мы хотим прослушать на предмет изменений. - Создайте объект
FileAlterationObserver
, который будет наблюдать за этими изменениями. - Добавьте
FileAlterationListenerAdaptor
к наблюдателю, используя методaddListener()
. Вы можете создать адаптер различными способами, но в нашем примере мы использовали вложенный класс, который реализует только некоторые методы (те, которые нам нужны для требований примера). - Создайте
FileAlterationMonitor
и добавьте наблюдателей, которые у вас есть, а также интервал (в мс). - Запустите монитор с помощью метода
start()
и остановите его при необходимости с помощью методаstop()
.
1.3 Фильтры
Фильтры могут использоваться в различных комбинациях и способах . Их задача — позволить нам легко различать файлы и получать те, которые удовлетворяют определенным критериям. Мы также можем комбинировать фильтры, чтобы выполнять логические сравнения и получать наши файлы гораздо точнее, не используя потом утомительные сравнения строк.
FiltersExample.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
import java.io.File; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.AndFileFilter; import org.apache.commons.io.filefilter.NameFileFilter; import org.apache.commons.io.filefilter.NotFileFilter; import org.apache.commons.io.filefilter.OrFileFilter; import org.apache.commons.io.filefilter.PrefixFileFilter; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.io.filefilter.WildcardFileFilter; public final class FiltersExample { private static final String PARENT_DIR = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder" ; public static void runExample() { System.out.println( "File Filter example..." ); // NameFileFilter // Right now, in the parent directory we have 3 files: // directory example // file exampleEntry.txt // file exampleTxt.txt // Get all the files in the specified directory // that are named "example". File dir = FileUtils.getFile(PARENT_DIR); String[] acceptedNames = { "example" , "exampleTxt.txt" }; for (String file: dir.list( new NameFileFilter(acceptedNames, IOCase.INSENSITIVE))) { System.out.println( "File found, named: " + file); } //WildcardFileFilter // We can use wildcards in order to get less specific results // ? used for 1 missing char // * used for multiple missing chars for (String file: dir.list( new WildcardFileFilter( "*ample*" ))) { System.out.println( "Wildcard file found, named: " + file); } // PrefixFileFilter // We can also use the equivalent of startsWith // for filtering files. for (String file: dir.list( new PrefixFileFilter( "example" ))) { System.out.println( "Prefix file found, named: " + file); } // SuffixFileFilter // We can also use the equivalent of endsWith // for filtering files. for (String file: dir.list( new SuffixFileFilter( ".txt" ))) { System.out.println( "Suffix file found, named: " + file); } // OrFileFilter // We can use some filters of filters. // in this case, we use a filter to apply a logical // or between our filters. for (String file: dir.list( new OrFileFilter( new WildcardFileFilter( "*ample*" ), new SuffixFileFilter( ".txt" )))) { System.out.println( "Or file found, named: " + file); } // And this can become very detailed. // Eg, get all the files that have "ample" in their name // but they are not text files (so they have no ".txt" extension. for (String file: dir.list( new AndFileFilter( // we will match 2 filters... new WildcardFileFilter( "*ample*" ), // ...the 1st is a wildcard... new NotFileFilter( new SuffixFileFilter( ".txt" ))))) { // ...and the 2nd is NOT .txt. System.out.println( "And/Not file found, named: " + file); } } } |
Выход
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
File Filter example... File found, named: example File found, named: exampleTxt.txt Wildcard file found, named: example Wildcard file found, named: exampleFileEntry.txt Wildcard file found, named: exampleTxt.txt Prefix file found, named: example Prefix file found, named: exampleFileEntry.txt Prefix file found, named: exampleTxt.txt Suffix file found, named: exampleFileEntry.txt Suffix file found, named: exampleTxt.txt Or file found, named: example Or file found, named: exampleFileEntry.txt Or file found, named: exampleTxt.txt And /Not file found, named: example |
1.4 Компараторы
Пакет org.apache.commons.io.comparator
содержит классы, которые позволяют нам легко сравнивать и сортировать файлы и каталоги. Нам просто нужно предоставить список файлов и, в зависимости от класса, сравнить их различными способами.
ComparatorExample.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
import java.io.File; import java.util.Date; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.comparator.LastModifiedFileComparator; import org.apache.commons.io.comparator.NameFileComparator; import org.apache.commons.io.comparator.SizeFileComparator; public final class ComparatorExample { private static final String PARENT_DIR = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder" ; private static final String FILE_1 = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\example" ; private static final String FILE_2 = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\ExampleFolder\\exampleTxt.txt" ; public static void runExample() { System.out.println( "Comparator example..." ); //NameFileComparator // Let's get a directory as a File object // and sort all its files. File parentDir = FileUtils.getFile(PARENT_DIR); NameFileComparator comparator = new NameFileComparator(IOCase.SENSITIVE); File[] sortedFiles = comparator.sort(parentDir.listFiles()); System.out.println( "Sorted by name files in parent directory: " ); for (File file: sortedFiles) { System.out.println( "\t" + file.getAbsolutePath()); } // SizeFileComparator // We can compare files based on their size. // The boolean in the constructor is about the directories. // true: directory's contents count to the size. // false: directory is considered zero size. SizeFileComparator sizeComparator = new SizeFileComparator( true ); File[] sizeFiles = sizeComparator.sort(parentDir.listFiles()); System.out.println( "Sorted by size files in parent directory: " ); for (File file: sizeFiles) { System.out.println( "\t" + file.getName() + " with size (kb): " + file.length()); } // LastModifiedFileComparator // We can use this class to find which file was more recently modified. LastModifiedFileComparator lastModified = new LastModifiedFileComparator(); File[] lastModifiedFiles = lastModified.sort(parentDir.listFiles()); System.out.println( "Sorted by last modified files in parent directory: " ); for (File file: lastModifiedFiles) { Date modified = new Date(file.lastModified()); System.out.println( "\t" + file.getName() + " last modified on: " + modified); } // Or, we can also compare 2 specific files and find which one was last modified. // returns > 0 if the first file was last modified. // returns 0) System.out.println( "File " + file1.getName() + " was modified last because..." ); else System.out.println( "File " + file2.getName() + "was modified last because..." ); System.out.println( "\t" + file1.getName() + " last modified on: " + new Date(file1.lastModified())); System.out.println( "\t" + file2.getName() + " last modified on: " + new Date(file2.lastModified())); } } |
Выход
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Comparator example... Sorted by name files in parent directory: C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\comparator1.txt C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\comperator2.txt C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\example C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\exampleFileEntry.txt C:\Users\Lilykos\workspace\ApacheCommonsExample\ExampleFolder\exampleTxt.txt Sorted by size files in parent directory: example with size (kb): 0 exampleTxt.txt with size (kb): 87 exampleFileEntry.txt with size (kb): 503 comperator2.txt with size (kb): 1458 comparator1.txt with size (kb): 4436 Sorted by last modified files in parent directory: exampleTxt.txt last modified on: Sun Oct 26 14:02:22 EET 2014 example last modified on: Sun Oct 26 23:42:55 EET 2014 comparator1.txt last modified on: Tue Oct 28 14:48:28 EET 2014 comperator2.txt last modified on: Tue Oct 28 14:48:52 EET 2014 exampleFileEntry.txt last modified on: Tue Oct 28 14:53:50 EET 2014 File example was modified last because... example last modified on: Sun Oct 26 23:42:55 EET 2014 exampleTxt.txt last modified on: Sun Oct 26 14:02:22 EET 2014 |
Давайте посмотрим, какие классы были использованы здесь:
-
NameFileComparator
: сравнивает файлы в соответствии с их именем. -
SizeFileComparator
: сравнивает файлы по размеру. -
LastModifiedFileComparator
: Сравнивает файлы по дате их последнего изменения.
Также следует обратить внимание, что сравнение может происходить либо во всех каталогах (если они были отсортированы с помощью метода sort()
), либо отдельно для двух файлов (с помощью compare()
).
1.5 Ввод
Существуют различные реализации InputStream
в пакете org.apache.commons.io.input
. Мы собираемся исследовать один из наиболее полезных, TeeInputStream
, который принимает в качестве аргументов и InputStream
и OutputStream
, и автоматически копирует прочитанные байты из входных данных в выходной. Более того, используя третий логический аргумент, закрывая в конце только TeeInputStream
, два дополнительных потока также закрываются.
InputExample.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.io.input.XmlStreamReader; public final class InputExample { private static final String XML_PATH = "C:\\Users\\Lilykos\\workspace\\ApacheCommonsExample\\InputOutputExampleFolder\\web.xml" ; private static final String INPUT = "This should go to the output." ; public static void runExample() { System.out.println( "Input example..." ); XmlStreamReader xmlReader = null ; TeeInputStream tee = null ; try { // XmlStreamReader // We can read an xml file and get its encoding. File xml = FileUtils.getFile(XML_PATH); xmlReader = new XmlStreamReader(xml); System.out.println( "XML encoding: " + xmlReader.getEncoding()); // TeeInputStream // This very useful class copies an input stream to an output stream // and closes both using only one close() method (by defining the 3rd // constructor parameter as true). ByteArrayInputStream in = new ByteArrayInputStream(INPUT.getBytes( "US-ASCII" )); ByteArrayOutputStream out = new ByteArrayOutputStream(); tee = new TeeInputStream(in, out, true ); tee.read( new byte [INPUT.length()]); System.out.println( "Output stream: " + out.toString()); } catch (IOException e) { e.printStackTrace(); } finally { try { xmlReader.close(); } catch (IOException e) { e.printStackTrace(); } try { tee.close(); } catch (IOException e) { e.printStackTrace(); } } } } |
Выход
1
2
3
|
Input example... XML encoding: UTF-8 Output stream: This should go to the output. |
1.6 Выход
Подобно org.apache.commons.io.input
, org.apache.commons.io.output
имеет реализации OutputStream
, которые можно использовать во многих ситуациях. Очень интересным является TeeOutputStream
, который позволяет TeeOutputStream
выходной поток, или, другими словами, мы можем отправить входной поток на 2 разных выхода.
OutputExample.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import org.apache.commons.io.input.TeeInputStream; import org.apache.commons.io.output.TeeOutputStream; public final class OutputExample { private static final String INPUT = "This should go to the output." ; public static void runExample() { System.out.println( "Output example..." ); TeeInputStream teeIn = null ; TeeOutputStream teeOut = null ; try { // TeeOutputStream ByteArrayInputStream in = new ByteArrayInputStream(INPUT.getBytes( "US-ASCII" )); ByteArrayOutputStream out1 = new ByteArrayOutputStream(); ByteArrayOutputStream out2 = new ByteArrayOutputStream(); teeOut = new TeeOutputStream(out1, out2); teeIn = new TeeInputStream(in, teeOut, true ); teeIn.read( new byte [INPUT.length()]); System.out.println( "Output stream 1: " + out1.toString()); System.out.println( "Output stream 2: " + out2.toString()); } catch (IOException e) { e.printStackTrace(); } finally { // No need to close teeOut. When teeIn closes, it will also close its // Output stream (which is teeOut), which will in turn close the 2 // branches (out1, out2). try { teeIn.close(); } catch (IOException e) { e.printStackTrace(); } } } } |
Выход
1
2
3
|
Output example... Output stream 1: This should go to the output. Output stream 2: This should go to the output. |
2. Загрузите полный пример
Это было введение в Apache Commons IO , охватывающее большинство важных классов, которые предоставляют разработчикам простые решения. В этом обширном пакете есть много других возможностей, но, используя это вступление, вы получите общую идею и несколько полезных инструментов для ваших будущих проектов!