lose of ZLIB inputstream fails when using try with resources - java-8

i have function for decompressing zips with multiple entries. Sometimes an exception is catched that states "Unexpected end of ZLIB Input stream". In my opinion it is not possible because i am using try with resrsources.
private boolean decompress(final Path blf) {
final String pathToZip = blf.getParent().toString();
final byte[] buffer = new byte[8192];
try (ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(blf.toFile()))) {
//We will unzip files in this folder
if (!Files.exists(Paths.get(pathToZip))) {
Files.createDirectory(Paths.get(pathToZip));
}
ZipEntry entry = zipInputStream.getNextEntry();
final Path pathToFile = Paths.get(pathToZip + "\\" + entry.getName());
//Faster procedure if the file already exists
if (Files.exists((pathToFile))) {
loggerAnalysis.log(new LogRecord(Level.INFO, "[Analysis]",
"File already exists, skipping" + pathToFile));
return true;
}
//Iterate over entries
while (entry != null) {
//If directory then create a new directory in uncompressed folder
if (entry.isDirectory()) {
loggerAnalysis.log(new LogRecord(Level.INFO, "[Analysis]",
"Creating Directory:" + pathToFile));
Files.createDirectories(pathToFile);
} else {
Files.createFile(pathToFile);
loggerAnalysis.log(new LogRecord(Level.INFO, "[Analysis]",
"File unzip: " + pathToFile.getFileName()));
try (FileOutputStream fileOutputStream = new FileOutputStream(pathToFile.toString())) {
int len;
while ((len = zipInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, len);
}
}
entry = zipInputStream.getNextEntry();
}
}
return true;
} catch (final IOException e) {
loggerAnalysis.log(new LogRecord(Level.ERROR, "[Analysis]", e.getMessage()));
return false;
}
}
best regards

Related

is it possible to read the content of the file present in the ftp server? [duplicate]

This is re-worded from a previous question (which was probably a bit unclear).
I want to download a text file via FTP from a remote server, read the contents of the text file into a string and then discard the file. I don't need to actually save the file.
I am using the Apache Commons library so I have:
import org.apache.commons.net.ftp.FTPClient;
Can anyone help please, without simply redirecting me to a page with lots of possible answers on?
Not going to do the work for you, but once you have your connection established, you can call retrieveFile and pass it an OutputStream. You can google around and find the rest...
FTPClient ftp = new FTPClient();
...
ByteArrayOutputStream myVar = new ByteArrayOutputStream();
ftp.retrieveFile("remoteFileName.txt", myVar);
ByteArrayOutputStream
retrieveFile
Normally I'd leave a comment asking 'What have you tried?'. But now I'm feeling more generous :-)
Here you go:
private void ftpDownload() {
FTPClient ftp = null;
try {
ftp = new FTPClient();
ftp.connect(mServer);
try {
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
throw new Exception("Connect failed: " + ftp.getReplyString());
}
if (!ftp.login(mUser, mPassword)) {
throw new Exception("Login failed: " + ftp.getReplyString());
}
try {
ftp.enterLocalPassiveMode();
if (!ftp.setFileType(FTP.BINARY_FILE_TYPE)) {
Log.e(TAG, "Setting binary file type failed.");
}
transferFile(ftp);
} catch(Exception e) {
handleThrowable(e);
} finally {
if (!ftp.logout()) {
Log.e(TAG, "Logout failed.");
}
}
} catch(Exception e) {
handleThrowable(e);
} finally {
ftp.disconnect();
}
} catch(Exception e) {
handleThrowable(e);
}
}
private void transferFile(FTPClient ftp) throws Exception {
long fileSize = getFileSize(ftp, mFilePath);
InputStream is = retrieveFileStream(ftp, mFilePath);
downloadFile(is, buffer, fileSize);
is.close();
if (!ftp.completePendingCommand()) {
throw new Exception("Pending command failed: " + ftp.getReplyString());
}
}
private InputStream retrieveFileStream(FTPClient ftp, String filePath)
throws Exception {
InputStream is = ftp.retrieveFileStream(filePath);
int reply = ftp.getReplyCode();
if (is == null
|| (!FTPReply.isPositivePreliminary(reply)
&& !FTPReply.isPositiveCompletion(reply))) {
throw new Exception(ftp.getReplyString());
}
return is;
}
private byte[] downloadFile(InputStream is, long fileSize)
throws Exception {
byte[] buffer = new byte[fileSize];
if (is.read(buffer, 0, buffer.length)) == -1) {
return null;
}
return buffer; // <-- Here is your file's contents !!!
}
private long getFileSize(FTPClient ftp, String filePath) throws Exception {
long fileSize = 0;
FTPFile[] files = ftp.listFiles(filePath);
if (files.length == 1 && files[0].isFile()) {
fileSize = files[0].getSize();
}
Log.i(TAG, "File size = " + fileSize);
return fileSize;
}
You can just skip the download to local filesystem part and do:
FTPClient ftpClient = new FTPClient();
try {
ftpClient.connect(server, port);
ftpClient.login(user, pass);
ftpClient.enterLocalPassiveMode();
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
InputStream inputStream = ftpClient.retrieveFileStream("/folder/file.dat");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "Cp1252"));
while(reader.ready()) {
System.out.println(reader.readLine()); // Or whatever
}
inputStream.close();
} catch (IOException ex) {
ex.printStackTrace();
} finally {
try {
if (ftpClient.isConnected()) {
ftpClient.logout();
ftpClient.disconnect();
}
} catch (IOException ex) {
ex.printStackTrace();
}
}

Send multiple files from angular typescript to spring and return as zip folder for downloading?

I want to send multiple files in an array to spring and create a zip folder for downloading
UploadController:
#Autowired
StorageService storageService;
#PostMapping("/upload")
public ResponseEntity<ResponseMessage> uploadFiles(#RequestParam("files") MultipartFile[] files) {
String message = "";
try {
storageService.zip(files);
message = "Uploaded the files successfully";
return ResponseEntity.status(HttpStatus.OK).body(new ResponseMessage(message));
} catch (Exception e) {
message = "Fail to upload files!";
return ResponseEntity.status(HttpStatus.EXPECTATION_FAILED).body(new ResponseMessage(message));
}
}
StorageService
public void zip(MultipartFile[] files) {
List<Path> filepaths = new ArrayList();
for (MultipartFile file : files) {
Path filepath = Paths.get("my/tmp/dir", file.getOriginalFilename());
filepaths.add(filepath);
try (OutputStream os = Files.newOutputStream(filepath)) {
os.write(file.getBytes());
}
}
File zip = new File("path/to/my/zip");
try { zip.createNewFile(); }
FileOutputStream output = null;
try { output = new FileOutputStream(zip); }
ZipOutputStream out = new ZipOutputStream(output);
try {
for (Path filepath : filepaths) {
File f = new File(filepath);
FileInputStream input = new FileInputStream(f);
ZipEntry e = new ZipEntry(f.getName());
out.putNextEntry(e);
byte[] bytes = new byte[1024];
int length;
while((length = input.read(bytes)) >= 0) {
out.write(bytes, 0, length);
}
input.close();
}
out.close();
output.close();
}
}

Saving new File to any directory Groovy

I keep getting file or directory does not exist. I am running within a Groovy script that creates a Spring Application Context. I am easily reading in a different file using the same type of pathing. However, the file I am reading is in the class path of Spring. This script might be run by any number of people with different file systems, so I can't hard code a path. I need a relative path.
This is above in the class but important info.
private static String saveFilesToLocation = "/retrieve/";
Here is the code.
CSVReader reader = new CSVReader(new InputStreamReader(balanceFile), SEPARATOR)
String[] nextLine
int counter = 0;
while ((nextLine = reader.readNext()) != null) {
counter++
if (nextLine != null && (nextLine[0] != 'FileLocation') ) {
counter++;
try {
//Remove 0, only if client number start with "0".
String fileLocation = nextLine[0];
byte[] fileBytes = documentFileService.getFile(fileLocation);
if (fileBytes != null) {
String fileName = fileLocation.substring(fileLocation.indexOf("/") + 1, fileLocation.length());
File file = new File(saveFilesToLocation+fileLocation);
file.withOutputStream {
it.write fileBytes
}
println "$counter) Wrote file ${fileLocation} to ${saveFilesToLocation+fileLocation}"
} else {
println "$counter) UNABLE TO RETRIEVE FILE: $fileLocation";
}
} catch (Exception e) {
e.printStackTrace()
}
}
}
The paths in Strings have what I would expect in them, no extra characters.
UPDATE:
Thanks loteq Your answer would work too, and has better grooviness than our final result that worked. Since it is a sort of one off, we don't have the time to change to the nicer version you have.
Here is the code that worked for us, it is identical to above except the saveFilesToLocation is set to a directory that already exists now. The one before didn't exist and we would have needed to call mkdir like loteq
suggested.
private static String saveFilesToLocation = "/tmp/retrieve/";
CSVReader reader = new CSVReader(new InputStreamReader(balanceFile), SEPARATOR)
String[] nextLine
int counter = 0;
while ((nextLine = reader.readNext()) != null) {
if (nextLine != null && (nextLine[0] != 'FileLocation') ) {
counter++;
try {
//Remove 0, only if client number start with "0".
String fileLocation = nextLine[0];
byte[] fileBytes = documentFileService.getFile(fileLocation);
if (fileBytes != null) {
String fileName = fileLocation.substring(fileLocation.indexOf("/") + 1, fileLocation.length());
File file = new File(saveFilesToLocation+fileName);
file.withOutputStream {
it.write fileBytes
}
println "$counter) Wrote file ${fileLocation} to ${saveFilesToLocation+fileLocation}"
} else {
println "$counter) UNABLE TO RETRIEVE FILE: $fileLocation";
}
} catch (Exception e) {
e.printStackTrace()
}
} else {
counter++;
}
}
There seems to be something add in your code, but I can't be certain that it's a bug.
You compute a fileName and don't really use it to create the target file. Instead you just append the original path to the prefix saveFilesToLocation:
String fileName = fileLocation.substring(fileLocation.indexOf("/") + 1, fileLocation.length());
File file = new File(saveFilesToLocation+fileLocation);
This seems strange.
Then, if fileLocation contains directories that need to be created, then you need to mkdirs() them, otherwise you will get an error.
I will give you 2 snippets, one atht assumes that youir code above is buggy, te other that does what you do in a safer way, in idiomatic groovy.
First lets work with actual File objects instead if Strings:
private static File saveFilesToLocationDir = saveFilesToLocation as File
Version that supposes a bug in the above code:
private static String saveFilesToLocation = "/retrieve/";
private static File saveFilesToLocationDir = saveFilesToLocation as File
CSVReader reader = new CSVReader(new InputStreamReader(balanceFile), SEPARATOR)
String[] nextLine
int counter = 0;
while ((nextLine = reader.readNext()) != null) {
counter++
if (nextLine != null && (nextLine[0] != 'FileLocation')) {
counter++;
try {
//Remove 0, only if client number start with "0".
String fileLocation = nextLine[0];
byte[] fileBytes = documentFileService.getFile(fileLocation);
if (fileBytes != null) {
int firstSlash = fileLocation.indexOf("/") + 1
String fileName = fileLocation[firstSlash..-1]
File destination = new File(saveFilesToLocationDir, fileName)
destination.parentFile.mkdirs()
destination.withOutputStream { it << fileBytes }
println "$counter) Wrote file ${fileLocation} to ${destination.absolutePath}"
} else {
println "$counter) UNABLE TO RETRIEVE FILE: $fileLocation";
}
} catch (Exception e) {
e.printStackTrace()
}
}
}
Version that does not use the created fileName (like you):
private static String saveFilesToLocation = "/retrieve/";
private static File saveFilesToLocationDir = saveFilesToLocation as File
CSVReader reader = new CSVReader(new InputStreamReader(balanceFile), SEPARATOR)
String[] nextLine
int counter = 0;
while ((nextLine = reader.readNext()) != null) {
counter++
if (nextLine != null && (nextLine[0] != 'FileLocation')) {
counter++;
try {
//Remove 0, only if client number start with "0".
String fileLocation = nextLine[0];
byte[] fileBytes = documentFileService.getFile(fileLocation);
if (fileBytes != null) {
int firstSlash = fileLocation.indexOf("/") + 1
String fileName = fileLocation[firstSlash..-1]
File destination = new File(saveFilesToLocationDir, fileLocation)
destination.parentFile.mkdirs()
destination.withOutputStream { it << fileBytes }
println "$counter) Wrote file ${fileLocation} to ${destination.absolutePath}"
} else {
println "$counter) UNABLE TO RETRIEVE FILE: $fileLocation";
}
} catch (Exception e) {
e.printStackTrace()
}
}
}

Receive assets on Handheld

I'm sending files from wear to handheld side. I'm sending 4 or 5 files (2 of them are bigger) but everytime the handheld side only receives ONE of the bigger ones...
wear:
public void sendFile(String filename){
//BLE files
fileToArray("BLEdata/" + filename + "_RightDevice.txt", "/BLEdata/");
//send log file
fileToArray(filename + "_LOG.txt", "/LOGdata/");
//send GPS file
fileToArray("GPSdata/" + filename + "_GPS.txt", "/GPSdata/");
//send Report files
fileToArray("Report/" + filename + "_Report.txt", "/REPORTdata/");
//BLE files
fileToArray("BLEdata/" + filename + "_LeftDevice.txt", "/BLEdata/");
}
public void fileToArray(String filename, String path)
{
FileInputStream fileInputStream = null;
File file = new File(Environment.getExternalStorageDirectory() + "/TuneWear/" + filename);
System.out.println("PATH: " + file.getPath());
if(file.exists()){
byte[] bFile = new byte[(int) file.length()];
try {
//convert file into array of bytes
fileInputStream = new FileInputStream(file);
fileInputStream.read(bFile);
fileInputStream.close();
WearResultsActivity main = (WearResultsActivity) getActivity();
long time = main.getInitTime();
if(filename.contains("_RightDevice")){
new SendToDataLayerThread(path + time, bFile, 2).start();
}else if (filename.contains("_LeftDevice")){
new SendToDataLayerThread(path + time, bFile, 1).start();
} else
new SendToDataLayerThread(path + time, bFile).start();
}catch(Exception e){
e.printStackTrace();
}
} else System.out.println("Doesn't exist:\n" + file.getPath());
}
class SendToDataLayerThread extends Thread {
String path;
byte[] bFile;
int footSide;
// Constructor for sending data objects to the data layer
SendToDataLayerThread(String p, byte[] bytes, int footside) {
path = p;
bFile = bytes;
footSide = footside;
}
SendToDataLayerThread(String p, byte[] bytes) {
path = p;
bFile = bytes;
}
public void run() {
WearResultsActivity main = (WearResultsActivity) getActivity();
GoogleApiClient googleClient = main.getGoogleClient();
Asset asset = Asset.createFromBytes(bFile);
System.out.println(asset.toString());
PutDataMapRequest dataMap = PutDataMapRequest.create(path);
dataMap.getDataMap().putLong("timestamp", Calendar.getInstance().getTimeInMillis());
dataMap.getDataMap().putLong("/InitialTime", ((WearResultsActivity) getActivity()).getInitTime());
dataMap.getDataMap().putAsset("asset", asset);
if(footSide == 1) {
dataMap.getDataMap().putInt("footside", footSide);
System.out.println("DATAMAP COM LEFT " + footSide);
}else if (footSide == 2) {
dataMap.getDataMap().putInt("footside", footSide);
System.out.println("DATAMAP COM RIGHT " + footSide);
}
PutDataRequest request = dataMap.asPutDataRequest();
PendingResult<DataApi.DataItemResult> pendingResult = Wearable.DataApi.putDataItem(googleClient, request);
pendingResult.setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
#Override
public void onResult(DataApi.DataItemResult dataItemResult) {
System.out.println("RESULT");
}
});
//Wearable.DataApi.putDataItem(googleClient, request);
}
}
Handheld:
#Override
public void onDataChanged(DataEventBuffer dataEvents) {
System.out.println("COUNT " + dataEvents.getCount());
for (DataEvent event : dataEvents) {
DataItem item = event.getDataItem();
if (event.getType() == DataEvent.TYPE_CHANGED) {
// DataItem changed
if (item.getUri().getPath().contains(LOGdata)) {
final DataMapItem dataMapItem = DataMapItem.fromDataItem(item);
Asset asset = dataMapItem.getDataMap().getAsset("asset");
//TODO here
Wearable.DataApi.getFdForAsset(googleClient, asset).setResultCallback(
new ResultCallback<DataApi.GetFdForAssetResult>() {
#Override
public void onResult(DataApi.GetFdForAssetResult getFdForAssetResult) {
InputStream assetInputStream = getFdForAssetResult.getInputStream();
long initialTime = dataMapItem.getDataMap().getLong(KEY_INITIALTIME);
String fileName = new SimpleDateFormat("HH'h'mm'm'ss's'_dd-MM-yyyy").format(initialTime);
String dataPath = Environment.getExternalStorageDirectory().toString() + "/TuneWear/";
File myDir = new File(dataPath);
myDir.mkdirs();
File file = new File(myDir, "Run_" + fileName + "_LOG.txt");
System.out.println("FILE: " + file.getPath());
try {
FileOutputStream fOut = new FileOutputStream(file);
int nRead;
byte[] data = new byte[16384];
while ((nRead = assetInputStream.read(data, 0, data.length)) != -1) {
fOut.write(data, 0, nRead);
}
fOut.flush();
fOut.close();
} catch (IOException e) {
System.out.println("ERROR File write failed: " + e.toString());
}
}
}
);
} else if (item.getUri().getPath().contains(GPSdata)) {
final DataMapItem dataMapItem = DataMapItem.fromDataItem(item);
Asset asset = dataMapItem.getDataMap().getAsset("asset");
//TODO here
Wearable.DataApi.getFdForAsset(googleClient, asset).setResultCallback(
new ResultCallback<DataApi.GetFdForAssetResult>() {
#Override
public void onResult(DataApi.GetFdForAssetResult getFdForAssetResult) {
InputStream assetInputStream = getFdForAssetResult.getInputStream();
long initialTime = dataMapItem.getDataMap().getLong(KEY_INITIALTIME);
String fileName = new SimpleDateFormat("HH'h'mm'm'ss's'_dd-MM-yyyy").format(initialTime);
String dataPath = Environment.getExternalStorageDirectory().toString() + "/TuneWear/GPSdata/";
File myDir = new File(dataPath);
myDir.mkdirs();
File file = new File(myDir, "Run_" + fileName + "_GPS.txt");
System.out.println("FILE: " + file.getPath());
try {
FileOutputStream fOut = new FileOutputStream(file);
int nRead;
byte[] data = new byte[16384];
while ((nRead = assetInputStream.read(data, 0, data.length)) != -1) {
fOut.write(data, 0, nRead);
}
fOut.flush();
fOut.close();
} catch (IOException e) {
System.out.println("ERROR File write failed: " + e.toString());
}
}
}
);
} else if (item.getUri().getPath().contains(REPORTdata)) {
final DataMapItem dataMapItem = DataMapItem.fromDataItem(item);
Asset asset = dataMapItem.getDataMap().getAsset("asset");
//TODO here
Wearable.DataApi.getFdForAsset(googleClient, asset).setResultCallback(
new ResultCallback<DataApi.GetFdForAssetResult>() {
#Override
public void onResult(DataApi.GetFdForAssetResult getFdForAssetResult) {
InputStream assetInputStream = getFdForAssetResult.getInputStream();
long initialTime = dataMapItem.getDataMap().getLong(KEY_INITIALTIME);
String fileName = new SimpleDateFormat("HH'h'mm'm'ss's'_dd-MM-yyyy").format(initialTime);
String dataPath = Environment.getExternalStorageDirectory().toString() + "/TuneWear/Report/";
File myDir = new File(dataPath);
myDir.mkdirs();
File file = new File(myDir, "Run_" + fileName + "_Report.txt");
System.out.println("FILE: " + file.getPath());
try {
FileOutputStream fOut = new FileOutputStream(file);
int nRead;
byte[] data = new byte[16384];
while ((nRead = assetInputStream.read(data, 0, data.length)) != -1) {
fOut.write(data, 0, nRead);
}
fOut.flush();
fOut.close();
} catch (IOException e) {
System.out.println("ERROR File write failed: " + e.toString());
}
}
}
);
} else if (item.getUri().getPath().contains(BLEdata)) {
final DataMapItem dataMapItem = DataMapItem.fromDataItem(item);
Asset asset = dataMapItem.getDataMap().getAsset("asset");
//TODO here
Wearable.DataApi.getFdForAsset(googleClient, asset).setResultCallback(
new ResultCallback<DataApi.GetFdForAssetResult>() {
#Override
public void onResult(DataApi.GetFdForAssetResult getFdForAssetResult) {
InputStream assetInputStream = getFdForAssetResult.getInputStream();
long initialTime = dataMapItem.getDataMap().getLong(KEY_INITIALTIME);
String fileName = new SimpleDateFormat("HH'h'mm'm'ss's'_dd-MM-yyyy").format(initialTime);
String dataPath = Environment.getExternalStorageDirectory().toString() + "/TuneWear/BLEdata/";
File myDir = new File(dataPath);
myDir.mkdirs();
File file = null;
System.out.println("FOOT SIIIIIIDE: " + dataMapItem.getDataMap().getInt("footside"));
if(dataMapItem.getDataMap().getInt("footside") == 1){
file = new File(myDir, "Run_" + fileName + "_LeftDevice.txt");
System.out.println("FILE: " + file.getPath());
} else if(dataMapItem.getDataMap().getInt("footside") == 2){
file = new File(myDir, "Run_" + fileName + "_RightDevice.txt");
System.out.println("FILE: " + file.getPath());
}
try {
FileOutputStream fOut = new FileOutputStream(file);
int nRead;
byte[] data = new byte[16384];
while ((nRead = assetInputStream.read(data, 0, data.length)) != -1) {
fOut.write(data, 0, nRead);
}
fOut.flush();
fOut.close();
}
catch (IOException e) {
System.out.println("ERROR File write failed: " + e.toString());
}
}
}
);
}
} else if (event.getType() == DataEvent.TYPE_DELETED) {
// DataItem deleted
System.out.println("DataItem deleted: " + event.getDataItem().getUri());
}
//Wearable.DataApi.deleteDataItems(googleClient, event.getDataItem().getUri(), DataApi.FILTER_PREFIX);
}
}
I only receive the GPS, Report and LOG data every time. The other 2 files I only receive ONE... Im sending them exactly the same way, but I'm receiving only one of them.
Does anyone detects the error on my code???
EDIT
I just discovered that if the smartwatch is connected to the handheld at the time that the files are sent, they are all received. If they are not connected, one of the files (RightDevice.txt or LeftDevice.txt) are not received when they connect...
I solved it!
It was the most basic error of all.... I was sending the same path for the two files (RightDevice and LeftDevice) so only one of the objects was updated on the googleapiclient.

How to make a save action that checks whether a 'save-as' has already been performed

I have researched and tried to refer back to my fileChooser.getSeletedFile() in my save as action but can not work out how to check whether or not a file has been created. Here is my attempted code so far:
Save as code(works well):
public void Save_As() {
fileChooserTest.setApproveButtonText("Save");
int actionDialog = fileChooserTest.showOpenDialog(this);
File fileName = new File(fileChooserTest.getSelectedFile() + ".txt");
try {
if (fileName == null) {
return;
}
BufferedWriter outFile = new BufferedWriter(new FileWriter(fileName));
outFile.write(this.jTextArea2.getText());//put in textfile
outFile.flush(); // redundant, done by close()
outFile.close();
} catch (IOException ex) {
}
}
"Save" code doesn't work:
private void SaveActionPerformed(java.awt.event.ActionEvent evt) {
File f = fileChooserTest.getSelectedFile();
try {
if (f.exists()) {
BufferedWriter bw1 = new BufferedWriter(new FileWriter(fileChooserTest.getSelectedFile() + ".txt"));
bw1 = new BufferedWriter(new FileWriter(fileChooserTest.getSelectedFile() + ".txt"));
String text = ((JTextArea) jTabbedPane1.getSelectedComponent()).getText();
bw1.write(text);
bw1.close();
} else {
Save_As();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
Instead of storing an instance to the JFileChooser rather store an instance to the File (wich will be null before any save has been performed). In your SaveActionPerformed method check if the file is null. If it is null then do a Save_As and store the selected file in your file variable, if it is not null then do a normal save into the file.

Resources