/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * * Copyright 2006 - 2013 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.repository2.unified; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.pentaho.platform.api.repository2.unified.IRepositoryFileData; import org.pentaho.platform.api.repository2.unified.IUnifiedRepository; import org.pentaho.platform.api.repository2.unified.RepositoryFile; import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl; import org.pentaho.platform.api.repository2.unified.RepositoryFileTree; import org.pentaho.platform.api.repository2.unified.RepositoryRequest; import org.pentaho.platform.api.repository2.unified.data.node.DataNode; import org.pentaho.platform.api.repository2.unified.data.node.DataNode.DataPropertyType; import org.pentaho.platform.api.repository2.unified.data.node.DataProperty; import org.pentaho.platform.api.repository2.unified.data.node.NodeRepositoryFileData; import org.pentaho.platform.api.repository2.unified.data.simple.SimpleRepositoryFileData; import org.pentaho.platform.util.web.MimeHelper; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import static org.mockito.Matchers.*; import static org.mockito.Mockito.doReturn; /** * Consider using {@code MockUnifiedRepository} instead. * <p> * Test utilities for {@code IUnifiedRepository}. Assists with two phases of mocking: stubbing and verification. * Uses the <a href="http://code.google.com/p/mockito/">Mockito</a> mocking framework. * * <p> * Stubbing Examples: * </p> * <p> * 1. Stub getFile and getDataForRead: * </p> * * <pre> * stubGetFile( repo, "/public/file.txt" ); * stubGetData( repo, "/public/file.txt", "abcdefg" ); * // do test after stubbing * </pre> * * <p> * Verification Examples: * </p> * <p> * 1. Verify that createFile was called: * </p> * * <pre> * // do test before verifying * verify( repo ).createFile( eq( publicFolderId ), * argThat( isLikeFile( new RepositoryFile.Builder( fileName ).build() ) ), * argThat( hasData( bytes, APPLICATION_OCTET_STREAM ) ), anyString() ); * </pre> * * @author mlowery */ @SuppressWarnings( "nls" ) public class UnifiedRepositoryTestUtils { /** * Generates an ID from the given path. */ public static Serializable makeIdObject( final String path ) { // paths are unique; so we just need to differentiate between paths and ids return "id(" + path + ")"; } /** * Stubs a {@code createFile} call. */ public static void stubCreateFile( final IUnifiedRepository repo, final String path ) { final String parentPath = StringUtils.substringBeforeLast( path, RepositoryFile.SEPARATOR ); doReturn( makeFileObject( path, true ) ).when( repo ).createFile( eq( makeIdObject( parentPath ) ), argThat( isLikeFile( makeFileObject( path, false ) ) ), any( IRepositoryFileData.class ), anyString() ); } /** * Stubs a {@code createFolder} call. */ public static void stubCreateFolder( final IUnifiedRepository repo, final String path ) { final String parentPath = StringUtils.substringBeforeLast( path, RepositoryFile.SEPARATOR ); doReturn( makeFileObject( path, true ) ).when( repo ).createFolder( eq( makeIdObject( parentPath ) ), argThat( isLikeFile( makeFolderObject( path, false ) ) ), anyString() ); } /** * Creates a {@code RepositoryFile}. */ public static RepositoryFile makeFileObject( final String path ) { return makeFileObject( path, false ); } /** * Creates a {@code RepositoryFile}. If {@code fromRepo} is {@code true}, then it is populated as if it had been * returned from the repository (e.g. non-null ID). */ public static RepositoryFile makeFileObject( final String path, final boolean fromRepo ) { final String fileName = StringUtils.substringAfterLast( path, RepositoryFile.SEPARATOR ); RepositoryFile.Builder b = new RepositoryFile.Builder( fileName ); // files returned from repo have non-null id and path properties if ( fromRepo ) { b.id( makeIdObject( path ) ).path( path ); } return b.build(); } /** * Same as {@link #makeFileObject(String)} except that the file is a folder. */ public static RepositoryFile makeFolderObject( final String path ) { return makeFolderObject( path, false ); } /** * Same as {@link #makeFileObject(String, boolean)} except that the file is a folder. */ public static RepositoryFile makeFolderObject( final String path, final boolean fromRepo ) { RepositoryFile.Builder b = new RepositoryFile.Builder( makeFileObject( path, fromRepo ) ); b.folder( true ); return b.build(); } /** * Stubs a {@code getFile} call. */ public static void stubGetFile( final IUnifiedRepository repo, final String path ) { stubGetFile( repo, path, false ); } /** * Stubs a {@code getFile} call where the resulting {@code RepositoryFile} should be a folder. */ public static void stubGetFolder( final IUnifiedRepository repo, final String path ) { stubGetFile( repo, path, true ); } /** * Stubs a {@code getFile} call. */ private static void stubGetFile( final IUnifiedRepository repo, final String path, final boolean folder ) { final String fileName = StringUtils.substringAfterLast( path, RepositoryFile.SEPARATOR ); RepositoryFile file = new RepositoryFile.Builder( makeIdObject( path ), fileName ).path( path ).folder( folder ).build(); doReturn( file ).when( repo ).getFile( path ); } /** * Stubs a {@code getChildren} call. {@code childrenNames} is zero or more file/folder names. A folder is * indicated by a trailing forward slash. * * <p> * Example: * </p> * * <pre> * stubGetChildren( repo, "/public", "hello/", "file1.txt" ); * </pre> */ public static void stubGetChildren( final IUnifiedRepository repo, RepositoryRequest request, final String... childrenNames ) { List<RepositoryFile> children = new ArrayList<RepositoryFile>( childrenNames.length ); for ( String childName : childrenNames ) { if ( childName.startsWith( RepositoryFile.SEPARATOR ) ) { throw new IllegalArgumentException( "child names must not begin with a forward slash" ); } final String fullChildPath = request.getPath() + RepositoryFile.SEPARATOR + ( childName.endsWith( RepositoryFile.SEPARATOR ) ? StringUtils.substringBefore( childName, RepositoryFile.SEPARATOR ) : childName ); RepositoryFile child = null; if ( childName.endsWith( RepositoryFile.SEPARATOR ) ) { child = makeFolderObject( fullChildPath, true ); } else { child = makeFileObject( fullChildPath, true ); } children.add( child ); } doReturn( children ).when( repo ).getChildren( request ); } /** * Stubs a {@code getChildren} call. {@code childrenNames} is zero or more file/folder names. A folder is * indicated by a trailing forward slash. * * <p> * Example: * </p> * * <pre> * stubGetChildren( repo, "/public", "hello/", "file1.txt" ); * </pre> */ public static void stubGetChildren( final IUnifiedRepository repo, final String path, final String... childrenNames ) { List<RepositoryFile> children = new ArrayList<RepositoryFile>( childrenNames.length ); for ( String childName : childrenNames ) { if ( childName.startsWith( RepositoryFile.SEPARATOR ) ) { throw new IllegalArgumentException( "child names must not begin with a forward slash" ); } final String fullChildPath = path + RepositoryFile.SEPARATOR + ( childName.endsWith( RepositoryFile.SEPARATOR ) ? StringUtils.substringBefore( childName, RepositoryFile.SEPARATOR ) : childName ); RepositoryFile child = null; if ( childName.endsWith( RepositoryFile.SEPARATOR ) ) { child = makeFolderObject( fullChildPath, true ); } else { child = makeFileObject( fullChildPath, true ); } children.add( child ); } doReturn( children ).when( repo ).getChildren( makeIdObject( path ) ); } /** * Stubs a {@code getDataForRead} call. The encoding is always {@code UTF-8}. The MIME type is auto-detected. */ public static void stubGetData( final IUnifiedRepository repo, final String path, final String text ) { final String encoding = "UTF-8"; byte[] bytes; try { bytes = text.getBytes( encoding ); } catch ( UnsupportedEncodingException e ) { throw new RuntimeException(); } doReturn( new AutoResetSimpleRepositoryFileData( new ByteArrayInputStream( bytes ), encoding, getMimeType( path ) ) ) .when( repo ).getDataForRead( makeIdObject( path ), SimpleRepositoryFileData.class ); } /** * Stubs a {@code getFile} call with a return value of {@code null}. While the mock framework will do this by * default, this method makes the stubbed call more explicit. */ public static void stubGetFileDoesNotExist( final IUnifiedRepository repo, final String path ) { doReturn( null ).when( repo ).getFile( path ); } /** * Stubs a {@code getDataForRead} call. The pairs specified will be used to construct a * {@code NodeRepositoryFileData} . */ public static void stubGetData( final IUnifiedRepository repo, final String path, final String rootNodeName, final PathPropertyPair... pairs ) { final String prefix = RepositoryFile.SEPARATOR + rootNodeName; DataNode rootNode = new DataNode( rootNodeName ); for ( PathPropertyPair pair : pairs ) { if ( !pair.getPath().startsWith( prefix ) ) { throw new IllegalArgumentException( "all paths must have a common prefix" ); } String[] pathSegments = pair.getPath().substring( prefix.length() + 1 ).split( "/" ); addChild( rootNode, pair.getProperty(), pathSegments, 0 ); } doReturn( new NodeRepositoryFileData( rootNode ) ).when( repo ).getDataForRead( makeIdObject( path ), NodeRepositoryFileData.class ); } private static void addChild( final DataNode rootNode, final DataProperty property, final String[] pathSegments, final int currentSegmentIndex ) { int currentSegmentIdx = currentSegmentIndex; if ( rootNode.hasNode( pathSegments[currentSegmentIdx] ) ) { addChild( rootNode.getNode( pathSegments[currentSegmentIdx] ), property, pathSegments, ++currentSegmentIdx ); return; } if ( ( currentSegmentIdx < pathSegments.length - 1 ) || ( currentSegmentIdx == pathSegments.length - 1 && property == null ) ) { rootNode.addNode( pathSegments[currentSegmentIdx] ); } else { switch ( property.getType() ) { case BOOLEAN: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getBoolean() ); break; case DATE: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getDate() ); break; case DOUBLE: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getDouble() ); break; case LONG: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getLong() ); break; case REF: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getRef() ); break; default: rootNode.setProperty( pathSegments[currentSegmentIdx], property.getString() ); break; } } if ( currentSegmentIdx < pathSegments.length - 1 ) { addChild( rootNode.getNode( pathSegments[currentSegmentIdx] ), property, pathSegments, ++currentSegmentIdx ); } } /** * Stubs a {@code getTree} call. {@code rootPath} is the root of the tree to return. {@code paths} is zero or * more paths, relative to {@code rootPath} (i.e. they must not begin with a forward slash). Intermediate folders * are created automatically. To specify an empty folder, end the path with a forward slash. * * <p> * Example: * </p> * * <pre> * stubGetTree( repo, "/public", "relUrlTest.url", "hello/file.txt", * "hello/file2.txt", "hello2/" ); * </pre> */ public static void stubGetTree( final IUnifiedRepository repo, final String rootPath, final String... paths ) { RepositoryFileTree.Builder root = new RepositoryFileTree.Builder( makeFolderObject( rootPath, true ) ); for ( String path : paths ) { if ( path.startsWith( RepositoryFile.SEPARATOR ) ) { throw new IllegalArgumentException( "all paths must be relative" ); } String[] pathSegments = path.split( RepositoryFile.SEPARATOR ); boolean leafIsFolder = path.endsWith( RepositoryFile.SEPARATOR ); addChild( root, leafIsFolder, pathSegments, 0 ); } doReturn( root.build() ).when( repo ).getTree( eq( rootPath ), anyInt(), anyString(), anyBoolean() ); } /** * Helper method to construct a {@link RepositoryFileTree} from a list of paths. */ private static void addChild( final RepositoryFileTree.Builder root, final boolean leafIsFolder, final String[] pathSegments, final int currentSegmentIndex ) { int currentSegmentIdx = currentSegmentIndex; for ( RepositoryFileTree.Builder child : root.getChildren() ) { if ( child.getFile().isFolder() && child.getFile().getName().equals( pathSegments[currentSegmentIdx] ) ) { if ( currentSegmentIdx < pathSegments.length - 1 ) { addChild( child, leafIsFolder, pathSegments, ++currentSegmentIdx ); return; } } } String reconstructedPath = root.getFile().getPath() + RepositoryFile.SEPARATOR + StringUtils.join( Arrays.copyOfRange( pathSegments, 0, currentSegmentIdx + 1 ), RepositoryFile.SEPARATOR ); RepositoryFile file = null; if ( ( currentSegmentIdx < pathSegments.length - 1 ) || ( currentSegmentIdx == pathSegments.length - 1 && leafIsFolder ) ) { file = makeFolderObject( reconstructedPath, true ); } else { file = makeFileObject( reconstructedPath, true ); } RepositoryFileTree.Builder newChild = new RepositoryFileTree.Builder( file ); root.child( newChild ); if ( currentSegmentIdx < pathSegments.length - 1 ) { addChild( newChild, leafIsFolder, pathSegments, ++currentSegmentIdx ); } } /** * A {@link SimpleRepositoryFileData} that resets its stream every time the stream is requested. */ @SuppressWarnings( "serial" ) private static class AutoResetSimpleRepositoryFileData extends SimpleRepositoryFileData { public AutoResetSimpleRepositoryFileData( final InputStream stream, final String encoding, final String mimeType ) { super( stream, encoding, mimeType ); if ( !stream.markSupported() ) { throw new RuntimeException( "mark() must be supported" ); } stream.mark( Integer.MAX_VALUE ); } @Override public InputStream getStream() { InputStream stream = super.getStream(); try { stream.reset(); } catch ( IOException e ) { throw new RuntimeException( e ); } return stream; } } /** * Helper method to determine MIME type given a path. */ private static String getMimeType( final String path ) { String mimeType = MimeHelper.getMimeTypeFromFileName( path ); if ( mimeType == null ) { return "text/plain"; } else { return mimeType; } } /** * <a href="http://code.google.com/p/hamcrest/">Hamcrest</a> matcher for {@link NodeRepositoryFileData}. Use * factory method to create. */ private static class NodeRepositoryFileDataMatcher extends TypeSafeMatcher<NodeRepositoryFileData> { private static final String shortName = "hasData"; private PathPropertyPair[] pairs; public NodeRepositoryFileDataMatcher( PathPropertyPair... pairs ) { for ( PathPropertyPair pair : pairs ) { checkPath( pair.getPath() ); } // defensive copy this.pairs = Arrays.copyOf( pairs, pairs.length ); } @Override public boolean matchesSafely( final NodeRepositoryFileData data ) { for ( PathPropertyPair pair : pairs ) { DataProperty expectedProperty = pair.getProperty(); String[] pathSegments = pair.getPath().substring( 1 ).split( "/" ); DataNode currentNode = data.getNode(); if ( !currentNode.getName().equals( pathSegments[0] ) ) { return false; } for ( int i = 1; i < pathSegments.length - 1; i++ ) { currentNode = currentNode.getNode( pathSegments[i] ); if ( currentNode == null ) { return false; } } DataProperty actualProperty = currentNode.getProperty( pathSegments[pathSegments.length - 1] ); if ( !expectedProperty.equals( actualProperty ) ) { return false; } } return true; } @Override public void describeTo( final Description description ) { description.appendText( shortName ); description.appendText( "(" ); description.appendText( "pathPropertyPairs=" ); description.appendText( Arrays.toString( pairs ) ); description.appendText( ")" ); } } /** * <a href="http://code.google.com/p/hamcrest/">Hamcrest</a> matcher for {@link SimpleRepositoryFileData}. Use * factory method to create. */ private static class SimpleRepositoryFileDataMatcher extends TypeSafeMatcher<SimpleRepositoryFileData> { private static final String shortName = "hasData"; private String expectedMimeType; private String expectedEncoding; private byte[] expectedBytes; public SimpleRepositoryFileDataMatcher( final byte[] expectedBytes, final String expectedEncoding, final String expectedMimeType ) { this.expectedBytes = expectedBytes; this.expectedEncoding = expectedEncoding; this.expectedMimeType = expectedMimeType; } @Override public boolean matchesSafely( final SimpleRepositoryFileData data ) { return streamMatches( data.getStream() ) && ObjectUtils.equals( expectedMimeType, data.getMimeType() ) && ObjectUtils.equals( expectedEncoding, data.getEncoding() ); } private boolean streamMatches( final InputStream stream ) { if ( stream == null ) { return expectedBytes.length == 0; } if ( !stream.markSupported() ) { throw new RuntimeException( "cannot test for match on stream that cannot be reset" ); } stream.mark( Integer.MAX_VALUE ); byte[] actualBytes = null; try { actualBytes = IOUtils.toByteArray( stream ); stream.reset(); // leave it like we found it } catch ( IOException e ) { throw new RuntimeException( e ); } return Arrays.equals( expectedBytes, actualBytes ); } public void describeTo( final Description description ) { final int MAX_EXCERPT_LENGTH = 10; description.appendText( shortName ); description.appendText( "(" ); if ( StringUtils.isNotBlank( expectedEncoding ) ) { description.appendText( "text=" ); String text = null; try { text = new String( expectedBytes, expectedEncoding ); } catch ( UnsupportedEncodingException e ) { throw new RuntimeException( e ); } description.appendText( head( text, MAX_EXCERPT_LENGTH ) ); description.appendText( "," ); description.appendText( "encoding=" ); description.appendText( expectedEncoding ); } else { description.appendText( "bytes=" ); description.appendText( head( expectedBytes, MAX_EXCERPT_LENGTH ) ); } description.appendText( "," ); description.appendText( "mimeType=" ); description.appendText( expectedMimeType ); description.appendText( ")" ); } /** * Returns at most {@code count} characters from {@code str}. */ private String head( final String str, final int count ) { if ( str.length() > count ) { return str.substring( 0, count ) + "..."; } else { return str; } } /** * Returns {@code String} representation of array consisting of at most {@code count} bytes from {@code bytes}. */ private String head( final byte[] bytes, final int count ) { if ( bytes.length > count ) { StringBuilder buf = new StringBuilder(); buf.append( "[" ); for ( int i = 0; i < count; i++ ) { if ( i > 0 ) { buf.append( ", " ); } buf.append( bytes[i] ); } buf.append( "..." ); buf.append( "]" ); return buf.toString(); } else { return Arrays.toString( bytes ); } } } /** * <a href="http://code.google.com/p/hamcrest/">Hamcrest</a> matcher for {@link RepositoryFileAcl}. Will only * attempt to match non-null properties. Use factory method to create. */ private static class SelectiveRepositoryFileAclMatcher extends TypeSafeMatcher<RepositoryFileAcl> { private static final String shortName = "isLikeAcl"; private RepositoryFileAcl expectedAcl; private boolean testAcesUsingEquals; /** * Creates an instance where {@code testAcesUsingEquals} is {@code false}. * * @param expectedAcl */ public SelectiveRepositoryFileAclMatcher( RepositoryFileAcl expectedAcl ) { this( expectedAcl, false ); } /** * @param expectedAcl * expected ACL * @param testAcesUsingEquals * if {@code true}, use {@code acl.getAces().equals(expectedAcl.getAces())} else * {@code acl.getAces().containsAll(expectedAcl.getAces())} */ public SelectiveRepositoryFileAclMatcher( final RepositoryFileAcl expectedAcl, final boolean testAcesUsingEquals ) { super(); this.expectedAcl = expectedAcl; this.testAcesUsingEquals = testAcesUsingEquals; } /* * RepositoryFileAcl.getAces() never returns null. Also, isEntriesInheriting is always tested. */ @Override public boolean matchesSafely( final RepositoryFileAcl acl ) { return ( expectedAcl.getId() != null ? expectedAcl.getId().equals( acl.getId() ) : true ) && expectedAcl.isEntriesInheriting() == acl.isEntriesInheriting() && ( testAcesUsingEquals ? acl.getAces().equals( expectedAcl.getAces() ) : acl.getAces().containsAll( expectedAcl.getAces() ) ); } @Override public void describeTo( final Description description ) { boolean appended = false; description.appendText( shortName ); description.appendText( "(" ); if ( expectedAcl.getId() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "id=" ); description.appendText( expectedAcl.getId().toString() ); appended = true; } description.appendText( appended ? "," : "" ); description.appendText( "isEntriesInheriting=" ); description.appendText( String.valueOf( expectedAcl.isEntriesInheriting() ) ); appended = true; if ( expectedAcl.getAces() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "aces=" ); description.appendText( expectedAcl.getAces().toString() ); appended = true; } description.appendText( ")" ); } } /** * <a href="http://code.google.com/p/hamcrest/">Hamcrest</a> matcher for {@link RepositoryFile}. Will only * attempt to match non-null properties. Use factory method to create. */ private static class SelectiveRepositoryFileMatcher extends TypeSafeMatcher<RepositoryFile> { private static final String shortName = "isLikeFile"; private RepositoryFile expectedFile; public SelectiveRepositoryFileMatcher( final RepositoryFile expectedFile ) { this.expectedFile = expectedFile; } /* * If you add comparisons here, add them in describeTo as well. */ @Override public boolean matchesSafely( final RepositoryFile file ) { return ( expectedFile.getId() != null ? expectedFile.getId().equals( file.getId() ) : true ) && ( expectedFile.getName() != null ? expectedFile.getName().equals( file.getName() ) : true ) && ( expectedFile.getTitle() != null ? expectedFile.getTitle().equals( file.getTitle() ) : true ) && ( expectedFile.getPath() != null ? expectedFile.getPath().equals( file.getPath() ) : true ) && ( expectedFile.getCreatedDate() != null ? expectedFile.getCreatedDate().equals( file.getCreatedDate() ) : true ) && ( expectedFile.getLastModifiedDate() != null ? expectedFile.getLastModifiedDate().equals( file.getLastModifiedDate() ) : true ) && ( expectedFile.getVersionId() != null ? expectedFile.getVersionId().equals( file.getVersionId() ) : true ) && ( expectedFile.getDeletedDate() != null ? expectedFile.getDeletedDate().equals( file.getDeletedDate() ) : true ); } @Override public void describeTo( final Description description ) { boolean appended = false; description.appendText( shortName ); description.appendText( "(" ); if ( expectedFile.getId() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "id=" ); description.appendText( expectedFile.getId().toString() ); appended = true; } if ( expectedFile.getName() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "name=" ); description.appendText( expectedFile.getName() ); appended = true; } if ( expectedFile.getTitle() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "title=" ); description.appendText( expectedFile.getTitle() ); appended = true; } if ( expectedFile.getPath() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "path=" ); description.appendText( expectedFile.getPath() ); appended = true; } if ( expectedFile.getCreatedDate() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "createdDate=" ); description.appendText( expectedFile.getCreatedDate().toString() ); appended = true; } if ( expectedFile.getLastModifiedDate() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "lastModifiedDate=" ); description.appendText( expectedFile.getLastModifiedDate().toString() ); appended = true; } if ( expectedFile.getVersionId() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "versionId=" ); description.appendText( expectedFile.getVersionId().toString() ); appended = true; } if ( expectedFile.getDeletedDate() != null ) { description.appendText( appended ? "," : "" ); description.appendText( "deletedDate=" ); description.appendText( expectedFile.getDeletedDate().toString() ); appended = true; } description.appendText( ")" ); } } /** * Factory for binary {@code SimpleRepositoryFileData} matcher. * * <p> * Example: * </p> * * <pre> * assertThat( simpleRepositoryFileData, hasData( byteArray, "application/pdf" ) ); * </pre> * * @param expectedBytes * expected bytes * @param expectedMimeType * expected MIME type * @return matcher */ public static <T> Matcher<SimpleRepositoryFileData> hasData( final byte[] expectedBytes, final String expectedMimeType ) { return new SimpleRepositoryFileDataMatcher( expectedBytes, null, expectedMimeType ); } /** * Factory for textual {@code SimpleRepositoryFileData} matcher. * * <p> * Example: * </p> * * <pre> * assertThat( simpleRepositoryFileData, hasData( "test123", "UTF-8", "text/plain" ) ); * </pre> * * @param expectedText * expected text * @param encoding * expected encoding * @param expectedMimeType * expected MIME type * @return matcher */ public static <T> Matcher<SimpleRepositoryFileData> hasData( final String expectedText, final String encoding, final String expectedMimeType ) { byte[] expectedBytes = null; try { expectedBytes = expectedText.getBytes( encoding ); } catch ( UnsupportedEncodingException e ) { throw new RuntimeException( e ); } return new SimpleRepositoryFileDataMatcher( expectedBytes, encoding, expectedMimeType ); } /** * Factory for {@code RepositoryFile} matcher. Only attempts to match on non-null properties. * * <p> * Example: * </p> * * <pre> * assertThat( repositoryFile, isLikeFile( new RepositoryFile.Builder( "123", * "test.txt" ).build() ) ); * </pre> * * @param expectedFile * expected file * @return matcher */ public static <T> Matcher<RepositoryFile> isLikeFile( final RepositoryFile expectedFile ) { return new SelectiveRepositoryFileMatcher( expectedFile ); } /** * Factory for {@code RepositoryFileAcl} matcher. Only attempts to match on non-null properties. * * <p> * Example: * </p> * * <pre> * assertThat( repositoryFileAcl, isLikeAcl( new RepositoryFileAcl.Builder( "admin", * RepositoryFileSid.Type.USER ).ace( * "suzy", RepositoryFileSid.Type.USER, RepositoryFilePermission.WRITE ).build() ), true ); * </pre> * * @param expectedAcl * expected ACL * @param testAcesUsingEquals * if {@code true}, use {@code acl.getAces().equals(expectedAcl.getAces())} else * {@code acl.getAces().containsAll(expectedAcl.getAces())} * @return */ public static <T> Matcher<RepositoryFileAcl> isLikeAcl( final RepositoryFileAcl expectedAcl, final boolean testAcesUsingEquals ) { return new SelectiveRepositoryFileAclMatcher( expectedAcl, testAcesUsingEquals ); } /** * Factory for {@code RepositoryFileAcl} matcher. Only attempts to match on non-null properties. Aces are tested * using {@code containsAll(Collection)}. * * <p> * Example: * </p> * * <pre> * assertThat( repositoryFileAcl, * isLikeAcl( new RepositoryFileAcl.Builder( "admin", RepositoryFileSid.Type.USER ).build() ) ); * </pre> * * @param expectedAcl * @return */ public static <T> Matcher<RepositoryFileAcl> isLikeAcl( final RepositoryFileAcl expectedAcl ) { return new SelectiveRepositoryFileAclMatcher( expectedAcl ); } /** * Factory for {@code NodeRepositoryFileData} matcher. Only attempts to match pairs given. * * <p> * Example: * </p> * * <pre> * assertThat( nodeRepositoryFileData, hasData( pathPropertyPair( "/databaseMeta/HOST_NAME", * "localhost" ) ) ); * </pre> * * @param pairs * path property pairs * @return matcher */ public static <T> Matcher<NodeRepositoryFileData> hasData( final PathPropertyPair... pairs ) { return new NodeRepositoryFileDataMatcher( pairs ); } /** * Helper method that throws {@code IllegalArgumentException} if path does not meet certain criteria. * * @param path * path to check */ private static void checkPath( final String path ) { if ( path == null ) { throw new IllegalArgumentException( "path cannot be null" ); } if ( !path.startsWith( "/" ) ) { throw new IllegalArgumentException( "paths must start with a slash" ); } if ( path.endsWith( "/" ) ) { throw new IllegalArgumentException( "paths must not end with a slash" ); } if ( path.trim().equals( "/" ) ) { throw new IllegalArgumentException( "path must be path to property" ); } } /** * Factory for {@link PathPropertyPair} instances. Creates a path property pair that represents an empty node. */ public static PathPropertyPair emptyNode( final String path ) { checkPath( path ); return new PathPropertyPair( path, null ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final String value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.STRING ) ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final boolean value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.BOOLEAN ) ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final long value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.LONG ) ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final double value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.DOUBLE ) ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final Date value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.DATE ) ); } /** * Factory for {@link PathPropertyPair} instances. */ public static PathPropertyPair pathPropertyPair( final String path, final Serializable value ) { checkPath( path ); String[] pathSegments = path.split( "/" ); return new PathPropertyPair( path, new DataProperty( pathSegments[pathSegments.length - 1], value, DataPropertyType.REF ) ); } /** * A path and property pair. A {@code null} property represents a node. */ public static class PathPropertyPair { private String path; private DataProperty property; /** * Constructs a path property pair. If {@code property} is {@code null} then this path represents a node. */ public PathPropertyPair( final String path, final DataProperty property ) { this.path = path; this.property = property; } private String getPath() { return path; } private DataProperty getProperty() { return property; } @Override public String toString() { return "PathPropertyPair [path=" + path + ", property=" + property + "]"; } } }