Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 109 additions & 71 deletions core/src/main/java/dev/vml/es/acm/core/repo/RepoResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
import dev.vml.es.acm.core.util.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import org.apache.commons.io.IOUtils;
Expand All @@ -35,6 +35,8 @@
*/
public class RepoResource {

public static final String FILE_MIME_TYPE_DEFAULT = "application/octet-stream";

private final Repo repo;

private final String path;
Expand Down Expand Up @@ -402,6 +404,50 @@ public RepoResource moveInPlace(RepoResource target, boolean replace) {
return target;
}

// AEM 6.5.0 has no 'resourceResolver.orderBefore' so adding own based on:
// https://github.com/apache/sling-org-apache-sling-jcr-resource/commit/3b8f01d226124417bdfdba4ca2086114a73a7c5d
public boolean orderBefore(String siblingName) {
Node parentNode = parent().requireNode();
String name = getName();
try {
long existingNodePosition = -1;
long siblingNodePosition = -1;
long index = 0;

NodeIterator nodeIterator = parentNode.getNodes();
while (nodeIterator.hasNext()) {
Node childNode = nodeIterator.nextNode();
String childName = childNode.getName();

if (childName.equals(name)) {
existingNodePosition = index;
}
if (siblingName != null && childName.equals(siblingName)) {
siblingNodePosition = index;
} else if (siblingName == null && childName.equals(name)) {
if (existingNodePosition == nodeIterator.getSize() - 1) {
return false;
}
}
index++;
}

if (siblingName != null
&& existingNodePosition >= 0
&& siblingNodePosition >= 0
&& existingNodePosition == siblingNodePosition - 1) {
return false;
}

parentNode.orderBefore(name, siblingName);
repo.commit(String.format("reordering resource '%s' before '%s'", path, siblingName));
repo.getLogger().info("Reordered resource '{}' before '{}'", path, siblingName);
return true;
} catch (RepositoryException e) {
throw new RepoException(String.format("Cannot reorder resource '%s' before '%s'", path, siblingName), e);
}
}

public RepoResource parent() {
String parentPath = parentPath();
if (parentPath == null) {
Expand Down Expand Up @@ -534,100 +580,92 @@ public RepoResourceState state() {
return new RepoResourceState(path, true, resource.getValueMap());
}

public RepoResource saveFile(String mimeType, Consumer<OutputStream> dataWriter) {
saveFileInternal(mimeType, null, dataWriter);
return this;
public RepoResource saveFile(InputStream data) {
return saveFile(FILE_MIME_TYPE_DEFAULT, data);
}

public RepoResource saveFile(String mimeType, File file) {
saveFile(mimeType, (OutputStream os) -> {
try (InputStream is = Files.newInputStream(file.toPath())) {
IOUtils.copy(is, os);
} catch (IOException e) {
throw new RepoException(String.format("Cannot write file '%s' to path '%s'!", file.getPath(), path), e);
}
});
public RepoResource saveFile(String mimeType, InputStream data) {
return saveFile(Collections.singletonMap(JcrConstants.JCR_MIMETYPE, mimeType), data);
}

public RepoResource saveFile(Map<String, Object> properties, InputStream data) {
saveFileInternal(properties, data, null);
return this;
}

public RepoResource saveFile(String mimeType, Object data) {
saveFileInternal(mimeType, data, null);
public RepoResource saveFile(Consumer<OutputStream> dataWriter) {
return saveFile(FILE_MIME_TYPE_DEFAULT, dataWriter);
}

public RepoResource saveFile(String mimeType, Consumer<OutputStream> dataWriter) {
return saveFile(Collections.singletonMap(JcrConstants.JCR_MIMETYPE, mimeType), dataWriter);
}

public RepoResource saveFile(Map<String, Object> properties, Consumer<OutputStream> dataWriter) {
saveFileInternal(properties, null, dataWriter);
return this;
}

private void saveFileInternal(String mimeType, Object data, Consumer<OutputStream> dataWriter) {
private void saveFileInternal(Map<String, Object> properties, InputStream data, Consumer<OutputStream> dataWriter) {
if (properties == null) {
properties = new HashMap<>();
}
if (!properties.containsKey(JcrConstants.JCR_MIMETYPE)) {
Map<String, Object> propertiesMutable = new HashMap<>(properties);
propertiesMutable.put(JcrConstants.JCR_MIMETYPE, FILE_MIME_TYPE_DEFAULT);
properties = propertiesMutable;
}

Resource mainResource = resolve();
try {
if (mainResource == null) {
String parentPath = StringUtils.substringBeforeLast(path, "/");
Resource parent = repo.getResourceResolver().getResource(parentPath);
if (parent == null) {
throw new RepoException(
String.format("Cannot save file as parent path '%s' does not exist!", parentPath));
}
String name = StringUtils.substringAfterLast(path, "/");
Map<String, Object> mainValues = new HashMap<>();
mainValues.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_FILE);
mainResource = repo.getResourceResolver().create(parent, name, mainValues);
if (mainResource != null) {
repo.getResourceResolver().delete(mainResource);
}

Map<String, Object> contentValues = new HashMap<>();
setFileContent(contentValues, mimeType, data, dataWriter);
repo.getResourceResolver().create(mainResource, JcrConstants.JCR_CONTENT, contentValues);
Resource parent = parent().resolve();
if (parent == null) {
throw new RepoException(
String.format("Cannot save file as parent path '%s' does not exist!", parentPath()));
}
String name = getName();
Map<String, Object> mainValues = new HashMap<>();
mainValues.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_FILE);
mainResource = repo.getResourceResolver().create(parent, name, mainValues);

repo.commit(String.format("creating file at path '%s'", path));
repo.getLogger().info("Created file at path '{}'", path);
} else {
Resource contentResource = mainResource.getChild(JcrConstants.JCR_CONTENT);
if (contentResource == null) {
Map<String, Object> contentValues = new HashMap<>();
setFileContent(contentValues, mimeType, data, dataWriter);
repo.getResourceResolver().create(mainResource, JcrConstants.JCR_CONTENT, contentValues);
} else {
ModifiableValueMap contentValues =
Objects.requireNonNull(contentResource.adaptTo(ModifiableValueMap.class));
setFileContent(contentValues, mimeType, data, dataWriter);
}
Map<String, Object> contentValues = new HashMap<>();
setFileContent(contentValues, properties, data, dataWriter);
repo.getResourceResolver().create(mainResource, JcrConstants.JCR_CONTENT, contentValues);

repo.commit(String.format("updating file at path '%s'", path));
repo.getLogger().info("Updated file at path '{}'", path);
}
repo.commit(String.format("saving file at path '%s'", path));
repo.getLogger().info("Saved file at path '{}'", path);
} catch (PersistenceException e) {
throw new RepoException(String.format("Cannot save file at path '%s'!", path), e);
}
}

private void setFileContent(
Map<String, Object> contentValues, String mimeType, Object data, Consumer<OutputStream> dataWriter) {
Map<String, Object> contentValues,
Map<String, Object> props,
InputStream data,
Consumer<OutputStream> dataWriter) {
contentValues.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_RESOURCE);
contentValues.putAll(props);

if (dataWriter != null) {
setFileContent(contentValues, mimeType, dataWriter);
final PipedInputStream pis = new PipedInputStream();
Executors.newSingleThreadExecutor().submit(() -> {
try (PipedOutputStream pos = new PipedOutputStream(pis)) {
dataWriter.accept(pos);
} catch (Exception e) {
throw new RepoException(String.format("Cannot write data to file at path '%s'!", path), e);
}
});
contentValues.put(JcrConstants.JCR_DATA, pis);
} else {
setFileContent(contentValues, mimeType, data);
contentValues.put(JcrConstants.JCR_DATA, data);
}
}

private void setFileContent(Map<String, Object> contentValues, String mimeType, Object data) {
contentValues.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_RESOURCE);
contentValues.put(JcrConstants.JCR_ENCODING, "utf-8");
contentValues.put(JcrConstants.JCR_MIMETYPE, mimeType);
contentValues.put(JcrConstants.JCR_DATA, data);
}

// https://stackoverflow.com/a/27172165
private void setFileContent(Map<String, Object> contentValues, String mimeType, Consumer<OutputStream> dataWriter) {
contentValues.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_RESOURCE);
contentValues.put(JcrConstants.JCR_ENCODING, "utf-8");
contentValues.put(JcrConstants.JCR_MIMETYPE, mimeType);
final PipedInputStream pis = new PipedInputStream();
Executors.newSingleThreadExecutor().submit(() -> {
try (PipedOutputStream pos = new PipedOutputStream(pis)) {
dataWriter.accept(pos);
} catch (Exception e) {
throw new RepoException(String.format("Cannot write data to file at path '%s'!", path), e);
}
});
contentValues.put(JcrConstants.JCR_DATA, pis);
}

public InputStream readFileAsStream() {
Resource resource = require();
Resource contentResource = resource.getChild(JcrConstants.JCR_CONTENT);
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/dev/vml/es/acm/core/script/Script.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import dev.vml.es.acm.core.AcmException;
import dev.vml.es.acm.core.code.ExecutableMetadata;
import dev.vml.es.acm.core.code.Executable;
import dev.vml.es.acm.core.code.ExecutableMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import dev.vml.es.acm.core.repo.RepoException;
import dev.vml.es.acm.core.repo.RepoResource;
import dev.vml.es.acm.core.util.ResourceSpliterator;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
Expand Down Expand Up @@ -55,10 +58,10 @@ private Resource getRoot(ScriptType type) throws RepoException {
}

public Script save(Code code) {
return save(code.getId(), code.getContent());
return save(code.getId(), new ByteArrayInputStream(code.getContent().getBytes(StandardCharsets.UTF_8)));
}

public Script save(String id, Object data) throws AcmException {
public Script save(String id, InputStream data) throws AcmException {
if (!ScriptType.byPath(id).isPresent()) {
throw new AcmException(String.format("Cannot save script '%s' at unsupported path!", id));
}
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/002-console.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ test.describe('Console', () => {
await page.getByRole('tab', { name: 'Output' }).click();
await expectExecutionProgressBarSucceeded(page);

const output = await readFromCodeEditor(page);
const output = await readFromCodeEditor(page, 'Console Output');
expectToHaveMultilineText(output, `
Hello World!
`);
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/003-tool-access.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ test.describe('Tool Access', () => {
await page.getByRole('tab', { name: 'Output' }).click();
await expectExecutionProgressBarSucceeded(page);

const output = await readFromCodeEditor(page);
const output = await readFromCodeEditor(page, 'Console Output');
expect(output).toContain('Setup complete!');

await newAemContext(browser, 'acm-test-user', 'test1234', async (testUserPage) => {
Expand Down
21 changes: 19 additions & 2 deletions test/e2e/004-history.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test.describe('History', () => {
await expect(firstRow.locator('[role="rowheader"]')).toContainText('Console');
await firstRow.click();
await page.getByRole('tab', { name: 'Output' }).click();
const firstOutput = await readFromCodeEditor(page);
const firstOutput = await readFromCodeEditor(page, 'Execution Output');
expect(firstOutput).toContain('Setup complete!');

await page.goto('/acm#/history');
Expand All @@ -27,7 +27,24 @@ test.describe('History', () => {
await expect(secondRow.locator('[role="rowheader"]')).toContainText('Console');
await secondRow.click();
await page.getByRole('tab', { name: 'Output' }).click();
const secondOutput = await readFromCodeEditor(page);
const secondOutput = await readFromCodeEditor(page, 'Execution Output');
expect(secondOutput).toContain('Hello World!');
});

test('Shows automatic script executions', async ({ page }) => {
await page.goto('/acm');
await page.getByRole('button', { name: 'History' }).click();

const grid = page.locator('[role="grid"][aria-label="Executions table"]');
await expect(grid).toBeVisible();
const rows = grid.locator('[role="row"]');

await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-20_once');
await expect(rows.nth(1)).toContainText('Script \'example/ACME-20_once\'');
await expect(rows.nth(1)).toContainText('succeeded');

await page.getByRole('searchbox', { name: 'Executable' }).fill('example/ACME-21_changed');
await expect(rows.nth(1)).toContainText('Script \'example/ACME-21_changed\'');
await expect(rows.nth(1)).toContainText('succeeded');
});
});
Loading