├── .gitignore ├── pom.xml ├── readme.txt ├── sample-app ├── AndroidManifest.xml ├── default.properties ├── pom.xml ├── res │ ├── drawable-hdpi │ │ └── icon.png │ ├── drawable-ldpi │ │ └── icon.png │ ├── drawable-mdpi │ │ └── icon.png │ ├── layout │ │ ├── author_entry.xml │ │ ├── author_header.xml │ │ ├── author_row.xml │ │ ├── book_entry.xml │ │ ├── book_header.xml │ │ ├── book_row.xml │ │ ├── entity_list.xml │ │ ├── publisher_entry.xml │ │ ├── publisher_header.xml │ │ └── publisher_row.xml │ └── values │ │ └── strings.xml └── src │ ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── scala │ │ └── android │ │ └── crud │ │ └── sample │ │ ├── AuthorEntityType.scala │ │ ├── BookEntityType.scala │ │ ├── Genre.scala │ │ ├── PublisherEntityType.scala │ │ └── SampleApplication.scala │ └── test │ └── scala │ └── com │ └── github │ └── scala │ └── android │ └── crud │ └── sample │ ├── GenerateLayouts.scala │ └── SampleApplicationSpec.scala └── scala-android-crud ├── AndroidManifest.xml ├── default.properties ├── pom.xml ├── readme.txt ├── res ├── drawable │ ├── android_camera_256.png │ └── icon.png ├── layout │ ├── entity_list.xml │ ├── simple_spinner_item.xml │ ├── test_entry.xml │ ├── test_header.xml │ └── test_row.xml └── values │ └── strings.xml └── src ├── main └── scala │ └── com │ └── github │ └── scala │ └── android │ └── crud │ ├── BaseCrudActivity.scala │ ├── CrudActivity.scala │ ├── CrudAndroidApplication.scala │ ├── CrudApplication.scala │ ├── CrudBackupAgent.scala │ ├── CrudListActivity.scala │ ├── CrudPersistence.scala │ ├── CrudType.scala │ ├── DerivedPersistenceFactory.scala │ ├── GeneratedPersistenceFactory.scala │ ├── NamingConventions.scala │ ├── ParentField.scala │ ├── PersistenceFactory.scala │ ├── PersistenceOperation.scala │ ├── SQLiteEntityPersistence.scala │ ├── SQLitePersistenceFactory.scala │ ├── action │ ├── ContextVars.scala │ ├── Operation.scala │ └── OptionsMenuActivity.scala │ ├── common │ ├── CachedFunction.scala │ ├── CalculatedIterator.scala │ ├── Common.scala │ ├── ListenerHolder.scala │ ├── PlatformTypes.scala │ ├── ReadyFuture.scala │ ├── Timing.scala │ └── UriPath.scala │ ├── generate │ ├── CrudUIGenerator.scala │ └── EntityFieldInfo.scala │ ├── persistence │ ├── CursorField.scala │ ├── CursorStream.scala │ ├── EntityPersistence.scala │ ├── EntityType.scala │ ├── IdPk.scala │ ├── PersistedType.scala │ └── SeqEntityPersistence.scala │ ├── validate │ └── Validation.scala │ └── view │ ├── AdapterCaching.scala │ ├── AndroidConversions.scala │ ├── AndroidResourceAnalyzer.scala │ ├── CapturedImageView.scala │ ├── EntityAdapter.scala │ ├── EntityView.scala │ ├── EnumerationView.scala │ ├── FieldLayout.scala │ ├── OnClickOperationSetter.scala │ ├── ViewField.scala │ ├── ViewIdField.scala │ └── ViewRef.scala └── test ├── java └── com │ └── github │ └── scala │ └── android │ └── crud │ └── testres │ └── R.java └── scala └── com └── github └── scala └── android └── crud ├── CrudActivitySpec.scala ├── CrudApplicationSpec.scala ├── CrudBackupAgentSpec.scala ├── CrudEasyMockSugar.scala ├── CrudListActivitySpec.scala ├── CrudMockitoSugar.scala ├── CrudPersistenceSpec.scala ├── CrudTypeActionsSpec.scala ├── CrudTypeSpec.scala ├── DerivedPersistenceFactorySpec.scala ├── GeneratedCrudTypeSpec.scala ├── MyCrudType.scala ├── MyEntityType.scala ├── ParentFieldSpec.scala ├── SQLitePersistenceFactorySpec.scala ├── action ├── ContextVarsSpec.scala └── OptionsMenuActivitySpec.scala ├── common ├── CommonSpec.scala └── UriPathSpec.scala ├── generate ├── CrudUIGeneratorSpec.scala └── EntityFieldInfoSpec.scala ├── persistence ├── CursorFieldSpec.scala ├── CursorStreamSpec.scala ├── EntityPersistenceSpec.scala └── PersistedTypeSpec.scala ├── testres ├── SiblingToR.scala └── subpackage │ └── ClassInSubpackage.scala ├── validate └── ValidationSpec.scala └── view ├── AndroidResourceAnalyzerSpec.scala ├── CapturedImageViewSpec.scala ├── EnumerationViewSpec.scala ├── FieldLayoutSpec.scala ├── OnClickOperationSetterSpec.scala └── ViewFieldSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | src_managed 4 | project/plugins/project 5 | project/boot/* 6 | */project/build/target 7 | */project/boot 8 | lib_managed 9 | target 10 | .#* 11 | _dump 12 | *.log 13 | tm*.lck 14 | tmp 15 | *.iws 16 | *.ipr 17 | *.iml 18 | .project 19 | .settings 20 | .classpath 21 | .idea 22 | .scala_dependencies 23 | multiverse.log 24 | .eprj 25 | seed.txt 26 | .sonar* -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 4.0.0 17 | com.github.epabst.scala-android-crud 18 | scala-android-crud-parent 19 | 0.3-alpha7-SNAPSHOT 20 | pom 21 | Scala Android Crud - Parent 22 | https://github.com/epabst/scala-android-crud 23 | 2011 24 | 25 | Eric Pabst 26 | 27 | 28 | 29 | Apache 2.0 License 30 | http://maven.apache.org/license.html 31 | 32 | 33 | 34 | scm:git:git://github.com/epabst/scala-android-crud.git 35 | scm:git:git@github.com:epabst/scala-android-crud.git 36 | https://github.com/epabst/scala-android-crud 37 | 38 | 39 | 40 | scala-tools.org 41 | http://nexus.scala-tools.org/content/repositories/releases/ 42 | 43 | 44 | scala-tools.org 45 | http://nexus.scala-tools.org/content/repositories/snapshots/ 46 | false 47 | 48 | 49 | 50 | 51 | true 52 | 53 | 54 | 55 | 56 | 57 | org.scala-tools 58 | maven-scala-plugin 59 | 2.15.2 60 | 61 | 62 | 63 | 64 | 65 | scala-android-crud 66 | sample-app 67 | 68 | 69 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | A Scala framework for supporting CRUD on the Android Platform between UI, Persistence, and Model 2 | 3 | See the Wiki for more information: 4 | https://github.com/epabst/scala-android-crud/wiki 5 | 6 | Here are some topics that will be added soon to the Wiki: 7 | 8 | * Getting Started: 9 | * CrudType and CrudApplication (refer to Scaladocs) 10 | * Naming Conventions for layout files, strings, DB tables, entityName, etc. 11 | * Supported Field Types: int, long, double, currency, Date, Calendar, String, Enumeration, 12 | * Supported Fields: both forms of viewId 13 | * How to indicate if the Entity is Updateable and/or Displayable 14 | * Parent Fields and Foreign Keys 15 | * Generating Layout (refer to Scaladocs) 16 | * UriPath (refer to Scaladocs) 17 | * Ready-to-use Enhancements: 18 | * Integrating with an Object Model: fields and findAll 19 | * DerivedCrudPersistence and overriding idField 20 | * Generated Fields 21 | * Backup Service 22 | * Under the Hood 23 | * Built-In Navigation, including Actions (refer to Scaladocs) 24 | * What subjects are copied to what subjects and when 25 | * Logging - SLF4J 26 | * How to Customize 27 | * Fields 28 | * Activity 29 | * Interacting with other Activities 30 | * Customizing Navigation 31 | * Future Features 32 | * Buttons and Links - For now these must be handled by customizing the app. 33 | -------------------------------------------------------------------------------- /sample-app/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample-app/default.properties: -------------------------------------------------------------------------------- 1 | # File used by Eclipse to determine the target system 2 | # Project target. 3 | target=android-7 -------------------------------------------------------------------------------- /sample-app/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epabst/scala-android-crud/659c9c2efdce7d5e1db6ab9cc4f4c3f55839f0eb/sample-app/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /sample-app/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epabst/scala-android-crud/659c9c2efdce7d5e1db6ab9cc4f4c3f55839f0eb/sample-app/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /sample-app/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epabst/scala-android-crud/659c9c2efdce7d5e1db6ab9cc4f4c3f55839f0eb/sample-app/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /sample-app/res/layout/author_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample-app/res/layout/author_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/res/layout/author_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/res/layout/book_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample-app/res/layout/book_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample-app/res/layout/book_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample-app/res/layout/entity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /sample-app/res/layout/publisher_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample-app/res/layout/publisher_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/res/layout/publisher_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /sample-app/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sample Application 4 | Author List 5 | Add Author 6 | Edit Author 7 | Book List 8 | Add Book 9 | Edit Book 10 | Publisher List 11 | Add Publisher 12 | Edit Publisher 13 | -------------------------------------------------------------------------------- /sample-app/src/main/scala/com/github/scala/android/crud/sample/AuthorEntityType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import com.github.scala.android.crud._ 4 | import persistence.CursorField._ 5 | import persistence.EntityType 6 | import view.ViewField._ 7 | import com.github.triangle._ 8 | import com.github.scala.android.crud.validate.Validation._ 9 | 10 | object AuthorEntityType extends EntityType { 11 | def entityName = "Author" 12 | 13 | def valueFields = List( 14 | persisted[String]("name") + viewId(classOf[R], "name", textView) + requiredString, 15 | 16 | viewId(classOf[R], "bookCount", intView) + 17 | bundleField[Int]("bookCount") + 18 | GetterFromItem[Int] { 19 | case UriField(Some(uri)) && CrudContextField(Some(crudContext)) => { 20 | println("calculating bookCount for " + uri + " and " + crudContext) 21 | crudContext.withEntityPersistence(BookEntityType) { persistence => 22 | val books = persistence.findAll(uri) 23 | Some(books.size) 24 | } 25 | } 26 | } 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /sample-app/src/main/scala/com/github/scala/android/crud/sample/BookEntityType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import com.github.scala.android.crud._ 4 | import persistence.CursorField._ 5 | import persistence.EntityType 6 | import persistence.PersistedType._ 7 | import java.util.Date 8 | import com.github.scala.android.crud.ParentField._ 9 | import view.ViewField._ 10 | import view.{EntityView, EnumerationView} 11 | import com.github.scala.android.crud.validate.Validation._ 12 | 13 | object BookEntityType extends EntityType { 14 | def entityName = "Book" 15 | 16 | def valueFields = List( 17 | foreignKey(AuthorEntityType), 18 | 19 | persisted[String]("name") + viewId(classOf[R], "name", textView) + requiredString, 20 | 21 | persisted[Int]("edition") + viewId(classOf[R], "edition", intView), 22 | 23 | persistedEnum[Genre.Value]("genre", Genre) + viewId(classOf[R], "genre", EnumerationView[Genre.Value](Genre)), 24 | 25 | foreignKey(PublisherEntityType) + viewId(classOf[R], "publisher", EntityView(PublisherEntityType)), 26 | 27 | persistedDate("publishDate") + viewId[Date](classOf[R], "publishDate", dateView) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /sample-app/src/main/scala/com/github/scala/android/crud/sample/Genre.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | /** A Book Genre. 4 | * @author pabstec 5 | */ 6 | 7 | object Genre extends Enumeration { 8 | val Fantasy = Value("Fantasy") 9 | val Romance = Value("Romance") 10 | val Child = Value("Child") 11 | val Nonfiction = Value("Non-Fiction") 12 | val SciFi = Value("Sci-Fi") 13 | val Other = Value("Other") 14 | } -------------------------------------------------------------------------------- /sample-app/src/main/scala/com/github/scala/android/crud/sample/PublisherEntityType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import com.github.scala.android.crud 4 | import crud._ 5 | import crud.persistence.CursorField._ 6 | import crud.persistence.EntityType 7 | import crud.view.ViewField._ 8 | import com.github.triangle._ 9 | import com.github.scala.android.crud.validate.Validation._ 10 | 11 | object PublisherEntityType extends EntityType { 12 | def entityName = "Publisher" 13 | 14 | def valueFields = List( 15 | persisted[String]("name") + viewId(classOf[R], "publisher_name", textView) + requiredString, 16 | 17 | viewId(classOf[R], "bookCount", intView) + bundleField[Int]("bookCount") + GetterFromItem[Int] { 18 | case UriField(Some(uri)) && CrudContextField(Some(crudContext)) => { 19 | println("calculating bookCount for " + uri + " and " + crudContext) 20 | crudContext.withEntityPersistence(BookEntityType) { persistence => 21 | val books = persistence.findAll(uri) 22 | Some(books.size) 23 | } 24 | } 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /sample-app/src/main/scala/com/github/scala/android/crud/sample/SampleApplication.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import com.github.scala.android.crud._ 4 | 5 | 6 | /** The sample application 7 | * @author Eric Pabst (epabst@gmail.com) 8 | */ 9 | class SampleApplication extends CrudApplication { 10 | val name = "Sample Application" 11 | 12 | val AuthorCrudType = new CrudType(AuthorEntityType, SQLitePersistenceFactory) 13 | val BookCrudType = new CrudType(BookEntityType, SQLitePersistenceFactory) 14 | val PublisherCrudType = new CrudType(PublisherEntityType, SQLitePersistenceFactory) 15 | 16 | def allCrudTypes = List(AuthorCrudType, BookCrudType, PublisherCrudType) 17 | 18 | def dataVersion = 2 19 | } 20 | 21 | class SampleAndroidApplication extends CrudAndroidApplication(new SampleApplication) 22 | 23 | class SampleBackupAgent extends CrudBackupAgent(new SampleApplication) -------------------------------------------------------------------------------- /sample-app/src/test/scala/com/github/scala/android/crud/sample/GenerateLayouts.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import com.github.scala.android.crud.generate.CrudUIGenerator 4 | 5 | /** A layout generator for the application. 6 | * @author Eric Pabst (epabst@gmail.com) 7 | */ 8 | 9 | object GenerateLayouts { 10 | def main(args: Array[String]) { 11 | CrudUIGenerator.generateLayouts(new SampleApplication, classOf[SampleAndroidApplication]) 12 | } 13 | } -------------------------------------------------------------------------------- /sample-app/src/test/scala/com/github/scala/android/crud/sample/SampleApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.sample 2 | 3 | import org.scalatest.Spec 4 | import org.scalatest.matchers.MustMatchers 5 | import com.github.scala.android.crud.persistence.CursorField._ 6 | import org.junit.runner.RunWith 7 | import org.scalatest.junit.JUnitRunner 8 | import org.scalatest.mock.MockitoSugar 9 | import org.mockito.Mockito._ 10 | import com.github.scala.android.crud._ 11 | import action.{ContextWithVars, ContextVars} 12 | 13 | /** A behavior specification for [[com.github.scala.android.crud.sample.AuthorEntityType]] 14 | * within [[com.github.scala.android.crud.sample.SampleApplication]]. 15 | * @author Eric Pabst (epabst@gmail.com) 16 | */ 17 | @RunWith(classOf[JUnitRunner]) 18 | class SampleApplicationSpec extends Spec with MustMatchers with MockitoSugar { 19 | val application = new SampleApplication 20 | 21 | describe("Author") { 22 | it("must have the right children") { 23 | application.AuthorCrudType.childEntities(application) must 24 | be (List[CrudType](application.BookCrudType)) 25 | } 26 | 27 | it("must calculate the book count") { 28 | val contextVars = new ContextVars {} 29 | val application = mock[CrudApplication] 30 | val crudContext = new CrudContext(mock[ContextWithVars], application) { 31 | override def vars = contextVars 32 | } 33 | val factory = GeneratedPersistenceFactory(new ListBufferCrudPersistence(Map.empty[String, Any], _, crudContext)) 34 | val bookCrudType = new CrudType(BookEntityType, factory) 35 | val bookPersistence = bookCrudType.openEntityPersistence(crudContext).asInstanceOf[ListBufferCrudPersistence[Map[String,Any]]] 36 | bookPersistence.buffer += Map.empty[String,Any] += Map.empty[String,Any] 37 | 38 | stub(application.crudType(BookEntityType)).toReturn(bookCrudType) 39 | val authorData = AuthorEntityType.copyAndTransformWithItem(List(AuthorEntityType.toUri(100L), crudContext), Map.empty[String,Any]) 40 | authorData must be (Map[String,Any](idFieldName -> 100L, "bookCount" -> 2)) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /scala-android-crud/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /scala-android-crud/default.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "build.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | # Project target. 11 | target=Google Inc.:Google APIs:8 12 | -------------------------------------------------------------------------------- /scala-android-crud/readme.txt: -------------------------------------------------------------------------------- 1 | Three major components: View, Persistence, Model (for business logic). 2 | They all share the same Entities with Fields but have their own PortableFields. 3 | View has UIContext (access to system UI environment and parent window), 4 | View instances, resource string and view ids for CRUD UI 5 | UIContext is NOT tied to an Entity, so it can support CRUD and UI on ANY Entity. 6 | 7 | Actions: 8 | create - creates Entry View in UIContext, copies from Unit to View, allows user input, then View to Table format, and save (insert), closes Entry View 9 | startCreate: EntryView, create(data): id, close(EntryView) 10 | read/list - [optional query: creates Query View in UIContext, possibly allows user query input, copies from View to Query] 11 | (or copies from model to simple Query), find/findAll, create List View, then copy Table format to View, user can close View 12 | startCriteria(criteria): CriteriaView, query(criteria): data stream, close(CriteriaView) 13 | startList(criteria): ListView, close(ListView), startRead(id): EntityView, read(id): Option[data], close(EntityView) 14 | update - (after read/list), create Entry View, copies from Table format to View, allows user input, 15 | copies from View to Table format, save (update), close Entry View 16 | startUpdate(id): EntryView, save(id, data), close(EntryView) 17 | delete - (after read/list) copies from View to simple value Query, optional prompt, delete, optional support for undo 18 | startDelete(id or criteria): DeleteView, delete(id or data stream), close(DeleteView) 19 | 20 | 21 | ToDos: 22 | * Make EntityPersistence.find(ID) return an Option[R] instead of just R. -------------------------------------------------------------------------------- /scala-android-crud/res/drawable/android_camera_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epabst/scala-android-crud/659c9c2efdce7d5e1db6ab9cc4f4c3f55839f0eb/scala-android-crud/res/drawable/android_camera_256.png -------------------------------------------------------------------------------- /scala-android-crud/res/drawable/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epabst/scala-android-crud/659c9c2efdce7d5e1db6ab9cc4f4c3f55839f0eb/scala-android-crud/res/drawable/icon.png -------------------------------------------------------------------------------- /scala-android-crud/res/layout/entity_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /scala-android-crud/res/layout/simple_spinner_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 20 | -------------------------------------------------------------------------------- /scala-android-crud/res/layout/test_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 11 | 13 | 14 | 15 | 17 | 19 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /scala-android-crud/res/layout/test_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | -------------------------------------------------------------------------------- /scala-android-crud/res/layout/test_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | -------------------------------------------------------------------------------- /scala-android-crud/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | scala-android-crud 4 | Add 5 | Edit 6 | Delete 7 | Undo Delete 8 | Saved 9 | Not Saved 10 | 11 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/BaseCrudActivity.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action._ 4 | import android.view.MenuItem 5 | import android.content.Intent 6 | import common.{UriPath, Common, Timing, PlatformTypes} 7 | import PlatformTypes._ 8 | import com.github.scala.android.crud.view.AndroidConversions._ 9 | import android.os.Bundle 10 | import com.github.triangle.{FieldList, PortableField, Logging} 11 | import view.{ViewField, OnClickOperationSetter} 12 | 13 | /** Support for the different Crud Activity's. 14 | * @author Eric Pabst (epabst@gmail.com) 15 | */ 16 | 17 | trait BaseCrudActivity extends ActivityWithVars with OptionsMenuActivity with Logging with Timing { 18 | def crudApplication: CrudApplication = super.getApplication.asInstanceOf[CrudAndroidApplication].application 19 | 20 | lazy val crudType: CrudType = crudApplication.allCrudTypes.find(crudType => Some(crudType.entityName) == currentUriPath.lastEntityNameOption).getOrElse { 21 | throw new IllegalStateException("No valid entityName in " + currentUriPath) 22 | } 23 | 24 | final def entityType = crudType.entityType 25 | 26 | override def setIntent(newIntent: Intent) { 27 | info("Current Intent: " + newIntent) 28 | super.setIntent(newIntent) 29 | } 30 | 31 | def currentUriPath: UriPath = { 32 | val defaultContentUri = crudApplication.defaultContentUri 33 | Option(getIntent).map(intent => Option(intent.getData).map(toUriPath(_)).getOrElse { 34 | // If no data was given in the intent (because we were started 35 | // as a MAIN activity), then use our default content provider. 36 | intent.setData(defaultContentUri) 37 | defaultContentUri 38 | }).getOrElse(defaultContentUri) 39 | } 40 | 41 | lazy val currentAction: String = getIntent.getAction 42 | 43 | def uriWithId(id: ID): UriPath = currentUriPath.specify(entityType.entityName, id.toString) 44 | 45 | lazy val crudContext = new CrudContext(this, crudApplication) 46 | 47 | def contextItems = List(currentUriPath, crudContext, PortableField.UseDefaults) 48 | 49 | def contextItemsWithoutUseDefaults = List(currentUriPath, crudContext) 50 | 51 | protected lazy val logTag = Common.tryToEvaluate(entityType.entityName).getOrElse(Common.logTag) 52 | 53 | /** This should be a lazy val in subclasses. */ 54 | protected def normalActions: Seq[Action] 55 | 56 | /** A ContextVar that holds an undoable Action if present. */ 57 | private object LastUndoable extends ContextVar[Undoable] 58 | 59 | def allowUndo(undoable: Undoable) { 60 | // Finish any prior undoable first. This could be re-implemented to support a stack of undoable operations. 61 | LastUndoable.clear(this).foreach(_.closeOperation.foreach(_.invoke(currentUriPath, this))) 62 | // Remember the new undoable operation 63 | LastUndoable.set(this, undoable) 64 | optionsMenuCommands = generateOptionsMenu.map(_.command) 65 | } 66 | 67 | protected def applicableActions: List[Action] = LastUndoable.get(this).map(_.undoAction).toList ++ normalActions 68 | 69 | protected lazy val normalOperationSetters: FieldList = { 70 | val setters = normalActions.filter(_.command.viewRef.isDefined).map(action => 71 | ViewField.viewId[Nothing](action.command.viewRef.get, OnClickOperationSetter(_ => action.operation))) 72 | FieldList.toFieldList(setters) 73 | } 74 | 75 | protected def bindNormalActionsToViews() { 76 | normalOperationSetters.defaultValue.copyTo(this, contextItems) 77 | } 78 | 79 | protected def generateOptionsMenu: List[Action] = 80 | applicableActions.filter(action => action.command.title.isDefined || action.command.icon.isDefined) 81 | 82 | def initialOptionsMenuCommands = generateOptionsMenu.map(_.command) 83 | 84 | override def onOptionsItemSelected(item: MenuItem): Boolean = { 85 | val actions = generateOptionsMenu 86 | actions.find(_.commandId == item.getItemId) match { 87 | case Some(action) => 88 | action.invoke(currentUriPath, this) 89 | if (LastUndoable.get(this).exists(_.undoAction.commandId == item.getItemId)) { 90 | LastUndoable.clear(this) 91 | optionsMenuCommands = generateOptionsMenu.map(_.command) 92 | } 93 | true 94 | case None => super.onOptionsItemSelected(item) 95 | } 96 | } 97 | 98 | override def onSaveInstanceState(outState: Bundle) { 99 | super.onSaveInstanceState(outState) 100 | // This is after the super call so that outState can be overridden if needed. 101 | crudContext.onSaveState(this, outState) 102 | } 103 | 104 | override def onRestoreInstanceState(savedInstanceState: Bundle) { 105 | // This is before the super call to be the opposite order as onSaveInstanceState. 106 | crudContext.onRestoreState(this, savedInstanceState) 107 | super.onRestoreInstanceState(savedInstanceState) 108 | } 109 | 110 | override def onResume() { 111 | trace("onResume") 112 | crudContext.onClearState(this, stayActive = true) 113 | super.onResume() 114 | } 115 | 116 | override def onDestroy() { 117 | crudContext.vars.onDestroyContext() 118 | super.onDestroy() 119 | } 120 | 121 | //available to be overridden for testing 122 | def openEntityPersistence(): CrudPersistence = crudType.openEntityPersistence(crudContext) 123 | 124 | def withPersistence[T](f: CrudPersistence => T): T = { 125 | val persistence = openEntityPersistence() 126 | try { 127 | f(persistence) 128 | } finally { 129 | persistence.close() 130 | } 131 | } 132 | 133 | override def toString = getClass.getSimpleName + "@" + System.identityHashCode(this) 134 | } 135 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/CrudActivity.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.{OperationResponse, EntityOperation} 4 | import android.os.Bundle 5 | import com.github.triangle.PortableField 6 | import android.content.Intent 7 | import android.app.Activity 8 | import com.github.scala.android.crud.view.AndroidConversions._ 9 | import android.widget.Toast 10 | import common.UriPath 11 | import validate.ValidationResult 12 | 13 | /** A generic Activity for CRUD operations 14 | * @author Eric Pabst (epabst@gmail.com) 15 | */ 16 | class CrudActivity extends BaseCrudActivity { self => 17 | 18 | private def populateFromUri(uri: UriPath) { 19 | future { 20 | withPersistence { persistence => 21 | val readableOrUnit: AnyRef = persistence.find(uri).getOrElse(PortableField.UseDefaults) 22 | val portableValue = entityType.copyFromItem(readableOrUnit :: contextItems) 23 | runOnUiThread(this) { portableValue.copyTo(this, contextItems) } 24 | } 25 | } 26 | } 27 | 28 | override def onCreate(savedInstanceState: Bundle) { 29 | super.onCreate(savedInstanceState) 30 | 31 | if (savedInstanceState == null) { 32 | setContentView(crudType.entryLayout) 33 | val currentPath = currentUriPath 34 | if (crudType.maySpecifyEntityInstance(currentPath)) { 35 | populateFromUri(currentPath) 36 | } else { 37 | entityType.copyFromItem(PortableField.UseDefaults :: contextItems, this) 38 | } 39 | } 40 | bindNormalActionsToViews() 41 | if (crudType.maySpecifyEntityInstance(currentUriPath)) { 42 | crudContext.addCachedStateListener(new CachedStateListener { 43 | def onClearState(stayActive: Boolean) { 44 | if (stayActive) { 45 | populateFromUri(currentUriPath) 46 | } 47 | } 48 | 49 | def onSaveState(outState: Bundle) { 50 | entityType.copy(this, outState) 51 | } 52 | 53 | def onRestoreState(savedInstanceState: Bundle) { 54 | val portableValue = entityType.copyFrom(savedInstanceState) 55 | runOnUiThread(self) { portableValue.copyTo(this, contextItems) } 56 | } 57 | }) 58 | } 59 | } 60 | 61 | override def onBackPressed() { 62 | // Save before going back so that the Activity being activated will read the correct data from persistence. 63 | val writable = crudType.newWritable 64 | withPersistence { persistence => 65 | val copyableFields = entityType.copyableTo(writable, contextItemsWithoutUseDefaults) 66 | val portableValue = copyableFields.copyFromItem(this :: contextItemsWithoutUseDefaults) 67 | if (portableValue.transform(ValidationResult.Valid).isValid) { 68 | val transformedWritable = portableValue.transform(writable) 69 | saveBasedOnUserAction(persistence, transformedWritable) 70 | } else { 71 | Toast.makeText(this, res.R.string.data_not_saved_since_invalid_notification, Toast.LENGTH_SHORT).show() 72 | } 73 | } 74 | super.onBackPressed() 75 | } 76 | 77 | private[crud] def saveBasedOnUserAction(persistence: CrudPersistence, writable: AnyRef) { 78 | try { 79 | val id = entityType.IdField.getter(currentUriPath) 80 | val newId = persistence.save(id, writable) 81 | Toast.makeText(this, res.R.string.data_saved_notification, Toast.LENGTH_SHORT).show() 82 | if (id.isEmpty) setIntent(getIntent.setData(uriWithId(newId))) 83 | } catch { case e => logError("onPause: Unable to store " + writable, e) } 84 | } 85 | 86 | protected lazy val normalActions = crudApplication.actionsForEntity(entityType).filter { 87 | case action: EntityOperation => action.entityName != entityType.entityName || action.action != currentAction 88 | case _ => true 89 | } 90 | 91 | override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { 92 | super.onActivityResult(requestCode, resultCode, data) 93 | if (resultCode == Activity.RESULT_OK) { 94 | //"this" is included in the list so that existing data isn't cleared. 95 | entityType.copyFromItem(List(OperationResponse(requestCode, data), crudContext, this), this) 96 | } else { 97 | debug("onActivityResult received resultCode of " + resultCode + " and data " + data + " for request " + requestCode) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/CrudAndroidApplication.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import android.app.Application 4 | 5 | /** 6 | * A CrudApplication for Android. 7 | * 8 | * Because this extends android.app.Application, it can't normally be instantiated 9 | * except on a device. Because of this, there is a convention 10 | * that each CrudApplication will have {{{class MyApplication extends CrudApplication {..} }}} that has its code, 11 | * then have {{{class MyAndroidApplication extends CrudAndroidApplication(new MyApplication)}}}. 12 | * 13 | * @author Eric Pabst (epabst@gmail.com) 14 | * Date: 3/2/12 15 | * Time: 5:07 PM 16 | */ 17 | abstract class CrudAndroidApplication(val application: CrudApplication) extends Application 18 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/CrudApplication.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action._ 4 | import common.{UriPath, Common} 5 | import java.util.NoSuchElementException 6 | import collection.mutable 7 | import persistence.EntityType 8 | import java.util.concurrent.CopyOnWriteArraySet 9 | import collection.JavaConversions._ 10 | import android.os.Bundle 11 | import com.github.triangle.{Field, Logging} 12 | import com.github.triangle.PortableField._ 13 | 14 | /** An Application that works with [[com.github.scala.android.crud.CrudType]]s. 15 | * @author Eric Pabst (epabst@gmail.com) 16 | * Date: 3/31/11 17 | * Time: 4:50 PM 18 | */ 19 | 20 | trait CrudApplication extends Logging { 21 | def logTag = Common.tryToEvaluate(nameId).getOrElse(Common.logTag) 22 | 23 | trace("Instantiated CrudApplication: " + this) 24 | 25 | def name: String 26 | 27 | /** The version of the data such as a database. This must be increased when new tables or columns need to be added, etc. */ 28 | def dataVersion: Int 29 | 30 | //this will be used for programmatic uses such as a database name 31 | lazy val nameId: String = name.replace(" ", "_").toLowerCase 32 | 33 | def classNamePrefix: String = getClass.getSimpleName.replace("$", "").stripSuffix("Application") 34 | def packageName: String = getClass.getPackage.getName 35 | 36 | /** All entities in the application, in priority order of most interesting first. */ 37 | def allCrudTypes: List[CrudType] 38 | def allEntityTypes: List[EntityType] = allCrudTypes.map(_.entityType) 39 | 40 | /** The EntityType for the first page of the App. */ 41 | def primaryEntityType: EntityType = allEntityTypes.head 42 | 43 | lazy val contentProviderAuthority = packageName 44 | // The first EntityType is used as the default starting point. 45 | lazy val defaultContentUri = UriPath("content://" + contentProviderAuthority) / primaryEntityType.entityName 46 | 47 | def childEntityTypes(entityType: EntityType): List[EntityType] = crudType(entityType).childEntityTypes(this) 48 | 49 | final def withEntityPersistence[T](entityType: EntityType, activity: ActivityWithVars)(f: CrudPersistence => T): T = { 50 | crudType(entityType).withEntityPersistence(new CrudContext(activity, this))(f) 51 | } 52 | 53 | def crudType(entityType: EntityType): CrudType = 54 | allCrudTypes.find(_.entityType == entityType).getOrElse(throw new NoSuchElementException(entityType + " not found")) 55 | 56 | def isListable(entityType: EntityType): Boolean = crudType(entityType).persistenceFactory.canList 57 | 58 | def isSavable(entityType: EntityType): Boolean = crudType(entityType).persistenceFactory.canSave 59 | 60 | def isAddable(entityType: EntityType): Boolean = isDeletable(entityType) 61 | 62 | def isDeletable(entityType: EntityType): Boolean = crudType(entityType).persistenceFactory.canDelete 63 | 64 | def actionsForEntity(entityType: EntityType): Seq[Action] = crudType(entityType).getEntityActions(this) 65 | 66 | def actionsForList(entityType: EntityType): Seq[Action] = crudType(entityType).getListActions(this) 67 | 68 | def actionToCreate(entityType: EntityType): Option[Action] = crudType(entityType).createAction 69 | 70 | def actionToUpdate(entityType: EntityType): Option[Action] = crudType(entityType).updateAction 71 | 72 | def actionToDelete(entityType: EntityType): Option[Action] = crudType(entityType).deleteAction 73 | 74 | def actionToList(entityType: EntityType): Option[Action] = Some(crudType(entityType).listAction) 75 | 76 | def actionToDisplay(entityType: EntityType): Option[Action] = Some(crudType(entityType).displayAction) 77 | } 78 | 79 | object CrudContextField extends Field(identityField[CrudContext]) 80 | object UriField extends Field(identityField[UriPath]) 81 | 82 | /** A listener for when a CrudContext is being destroyed and resources should be released. */ 83 | trait DestroyContextListener { 84 | def onDestroyContext() 85 | } 86 | 87 | /** A context which can store data for the duration of a single Activity. 88 | * @author Eric Pabst (epabst@gmail.com) 89 | */ 90 | 91 | case class CrudContext(context: ContextWithVars, application: CrudApplication) { 92 | def vars: ContextVars = context 93 | 94 | def openEntityPersistence(entityType: EntityType): CrudPersistence = 95 | application.crudType(entityType).openEntityPersistence(this) 96 | 97 | /** This is final so that it will call the similar method even when mocking, making mocking easier when testing. */ 98 | final def withEntityPersistence[T](entityType: EntityType)(f: CrudPersistence => T): T = 99 | withEntityPersistence_uncurried(entityType, f) 100 | 101 | /** This is useful for unit testing because it is much easier to mock than its counterpart. */ 102 | def withEntityPersistence_uncurried[T](entityType: EntityType, f: CrudPersistence => T): T = 103 | application.crudType(entityType).withEntityPersistence(this)(f) 104 | 105 | def addCachedStateListener(listener: CachedStateListener) { 106 | CachedStateListeners.get(context) += listener 107 | } 108 | 109 | def onSaveState(context: ContextVars, outState: Bundle) { 110 | CachedStateListeners.get(context).foreach(_.onSaveState(outState)) 111 | } 112 | 113 | def onRestoreState(context: ContextVars, savedState: Bundle) { 114 | CachedStateListeners.get(context).foreach(_.onRestoreState(savedState)) 115 | } 116 | 117 | def onClearState(context: ContextVars, stayActive: Boolean) { 118 | CachedStateListeners.get(context).foreach(_.onClearState(stayActive)) 119 | } 120 | } 121 | 122 | /** Listeners that represent state and will listen to a various events. */ 123 | object CachedStateListeners extends InitializedContextVar[mutable.Set[CachedStateListener]](new CopyOnWriteArraySet[CachedStateListener]()) 124 | 125 | trait CachedStateListener { 126 | /** Save any cached state into the given bundle before switching context. */ 127 | def onSaveState(outState: Bundle) 128 | 129 | /** Restore cached state from the given bundle before switching back context. */ 130 | def onRestoreState(savedInstanceState: Bundle) 131 | 132 | /** Drop cached state. If stayActive is true, then the state needs to be functional. */ 133 | def onClearState(stayActive: Boolean) 134 | } 135 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/CrudListActivity.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.Action 4 | import android.widget.ListView 5 | import _root_.android.app.ListActivity 6 | import android.os.Bundle 7 | import android.view.{ContextMenu, View, MenuItem} 8 | import android.view.ContextMenu.ContextMenuInfo 9 | import android.widget.AdapterView.AdapterContextMenuInfo 10 | import com.github.triangle.PortableValue 11 | import common.UriPath 12 | import persistence.PersistenceListener 13 | import common.PlatformTypes._ 14 | 15 | /** A generic ListActivity for CRUD operations 16 | * @author Eric Pabst (epabst@gmail.com) 17 | */ 18 | class CrudListActivity extends ListActivity with BaseCrudActivity { 19 | 20 | override def onCreate(savedInstanceState: Bundle) { 21 | super.onCreate(savedInstanceState) 22 | 23 | setContentView(crudType.listLayout) 24 | 25 | val view = getListView; 26 | view.setHeaderDividersEnabled(true); 27 | view.addHeaderView(getLayoutInflater.inflate(crudType.headerLayout, null)); 28 | bindNormalActionsToViews() 29 | registerForContextMenu(getListView) 30 | 31 | crudType.setListAdapterUsingUri(crudContext, this) 32 | future { 33 | populateFromParentEntities() 34 | crudType.addPersistenceListener(new PersistenceListener { 35 | def onDelete(uri: UriPath) { 36 | //Some of the parent fields may be calculated from the children 37 | populateFromParentEntities() 38 | } 39 | 40 | def onSave(id: ID) { 41 | //Some of the parent fields may be calculated from the children 42 | populateFromParentEntities() 43 | } 44 | }, this) 45 | } 46 | } 47 | 48 | private[crud] def populateFromParentEntities() { 49 | val uriPath = currentUriPath 50 | //copy each parent Entity's data to the Activity if identified in the currentUriPath 51 | val portableValues: List[PortableValue] = crudType.parentEntities(crudApplication).flatMap { parentType => 52 | if (parentType.maySpecifyEntityInstance(uriPath)) { 53 | parentType.copyFromPersistedEntity(uriPath, crudContext) 54 | } else { 55 | None 56 | } 57 | } 58 | runOnUiThread(this) { 59 | portableValues.foreach(_.copyTo(this, List(crudContext))) 60 | } 61 | } 62 | 63 | protected def contextMenuActions: Seq[Action] = crudApplication.actionsForEntity(entityType) match { 64 | case _ :: tail => tail.filter(_.command.title.isDefined) 65 | case Nil => Nil 66 | } 67 | 68 | override def onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo) { 69 | super.onCreateContextMenu(menu, v, menuInfo) 70 | val commands = contextMenuActions.map(_.command) 71 | for ((command, index) <- commands.zip(Stream.from(0))) 72 | menu.add(0, command.commandId, index, command.title.get) 73 | } 74 | 75 | override def onContextItemSelected(item: MenuItem) = { 76 | val actions = contextMenuActions 77 | val info = item.getMenuInfo.asInstanceOf[AdapterContextMenuInfo] 78 | actions.find(_.commandId == item.getItemId) match { 79 | case Some(action) => action.invoke(uriWithId(info.id), this); true 80 | case None => super.onContextItemSelected(item) 81 | } 82 | } 83 | 84 | protected lazy val normalActions = crudApplication.actionsForList(entityType) 85 | 86 | override def onListItemClick(l: ListView, v: View, position: Int, id: ID) { 87 | if (id >= 0) { 88 | crudApplication.actionsForEntity(entityType).headOption.map(_.invoke(uriWithId(id), this)).getOrElse { 89 | warn("There are no entity actions defined for " + entityType) 90 | } 91 | } else { 92 | debug("Ignoring " + entityType + ".onListItemClick(" + id + ")") 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/CrudPersistence.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.UriPath 4 | import common.Common 5 | import persistence._ 6 | import common.PlatformTypes._ 7 | import com.github.triangle.Logging 8 | 9 | /** An EntityPersistence for a CrudType. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | trait CrudPersistence extends EntityPersistence with Logging { 13 | override protected def logTag: String = Common.tryToEvaluate(entityType.logTag).getOrElse(Common.logTag) 14 | 15 | def entityType: EntityType 16 | 17 | def crudContext: CrudContext 18 | 19 | def toUri(id: ID) = entityType.toUri(id) 20 | 21 | def find[T <: AnyRef](uri: UriPath, instantiateItem: => T): Option[T] = 22 | find(uri).map(entityType.fieldsIncludingIdPk.copyAndTransform(_, instantiateItem)) 23 | 24 | /** Find an entity with a given ID using a baseUri. */ 25 | def find(id: ID, baseUri: UriPath): Option[AnyRef] = find(baseUri.specify(entityType.entityName, id.toString)) 26 | 27 | override def find(uri: UriPath): Option[AnyRef] = { 28 | val result = super.find(uri) 29 | info("find(" + uri + ") for " + entityType.entityName + " returned " + result) 30 | result 31 | } 32 | 33 | def findAll[T <: AnyRef](uri: UriPath, instantiateItem: => T): Seq[T] = 34 | findAll(uri).map(entityType.fieldsIncludingIdPk.copyAndTransform(_, instantiateItem)) 35 | 36 | /** Saves the entity. This assumes that the entityType's fields support copying from the given modelEntity. */ 37 | def save(modelEntity: IdPk): ID = { 38 | val writable = newWritable 39 | save(modelEntity.id, entityType.copyAndTransform(modelEntity, writable)) 40 | } 41 | } 42 | 43 | trait SeqCrudPersistence[T <: AnyRef] extends SeqEntityPersistence[T] with CrudPersistence 44 | 45 | class ListBufferCrudPersistence[T <: AnyRef](newWritableFunction: => T, val entityType: EntityType, val crudContext: CrudContext) 46 | extends ListBufferEntityPersistence[T](newWritableFunction) with SeqCrudPersistence[T] 47 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/DerivedPersistenceFactory.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.UriPath 4 | import persistence.{ReadOnlyPersistence, EntityType} 5 | 6 | /** A CrudPersistence that is derived from related CrudType persistence(s). 7 | * @author Eric Pabst (epabst@gmail.com) 8 | * @see DerivedPersistenceFactory 9 | */ 10 | abstract class DerivedCrudPersistence[T <: AnyRef](val crudContext: CrudContext, delegates: EntityType*) 11 | extends SeqCrudPersistence[T] with ReadOnlyPersistence { 12 | val delegatePersistenceMap: Map[EntityType,CrudPersistence] = 13 | delegates.map(delegate => delegate -> crudContext.openEntityPersistence(delegate)).toMap 14 | 15 | override def close() { 16 | delegatePersistenceMap.values.foreach(_.close()) 17 | super.close() 18 | } 19 | } 20 | 21 | /** A PersistenceFactory that is derived from related CrudType persistence(s). 22 | * @author Eric Pabst (epabst@gmail.com) 23 | */ 24 | abstract class DerivedPersistenceFactory[T <: AnyRef](delegates: EntityType*) extends GeneratedPersistenceFactory[T] { self => 25 | def findAll(entityType: EntityType, uri: UriPath, delegatePersistenceMap: Map[EntityType,CrudPersistence]): Seq[T] 26 | 27 | def createEntityPersistence(_entityType: EntityType, crudContext: CrudContext) = { 28 | new DerivedCrudPersistence[T](crudContext, delegates: _*) { 29 | def entityType = _entityType 30 | 31 | def findAll(uri: UriPath): Seq[T] = self.findAll(_entityType, uri, delegatePersistenceMap) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/GeneratedPersistenceFactory.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.CachedFunction 4 | import persistence.EntityType 5 | 6 | trait GeneratedPersistenceFactory[T <: AnyRef] extends PersistenceFactory { 7 | def canSave = false 8 | 9 | def newWritable: T = throw new UnsupportedOperationException("not supported") 10 | 11 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext): SeqCrudPersistence[T] 12 | } 13 | 14 | object GeneratedPersistenceFactory { 15 | def apply[T <: AnyRef](persistenceFunction: EntityType => SeqCrudPersistence[T]): GeneratedPersistenceFactory[T] = new GeneratedPersistenceFactory[T] { 16 | private val cachedPersistenceFunction = CachedFunction(persistenceFunction) 17 | 18 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext) = cachedPersistenceFunction(entityType) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/NamingConventions.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | /** A utility that defines the naming conventions for Crud applications. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | 7 | object NamingConventions { 8 | def toLayoutPrefix(entityName: String): String = entityName.collect { 9 | case c if (c.isUpper) => "_" + c.toLower 10 | case c if (Character.isJavaIdentifierPart(c)) => c.toString 11 | }.mkString.stripPrefix("_") 12 | } -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/ParentField.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.PlatformTypes._ 4 | import persistence.CursorField._ 5 | import android.provider.BaseColumns 6 | import com.github.triangle.{BaseField, DelegatingPortableField} 7 | import persistence.EntityType 8 | 9 | /** A ParentField to a CrudType. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | case class ParentField(entityType: EntityType) extends DelegatingPortableField[ID] { 13 | val fieldName = entityType.entityName.toLowerCase + BaseColumns._ID 14 | 15 | protected val delegate = entityType.UriPathId 16 | 17 | override def toString = "ParentField(" + entityType.entityName + ")" 18 | } 19 | 20 | object ParentField { 21 | def parentFields(field: BaseField): List[ParentField] = field.deepCollect { 22 | case parentField: ParentField => parentField 23 | } 24 | 25 | def foreignKey(entityType: EntityType) = { 26 | val parentField = ParentField(entityType) 27 | parentField + persisted[ID](parentField.fieldName) + sqliteCriteria[ID](parentField.fieldName) 28 | } 29 | } -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/PersistenceFactory.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.UriPath 4 | import persistence.EntityType 5 | 6 | /** A factory for EntityPersistence specific to a storage type such as SQLite. 7 | * @author Eric Pabst (epabst@gmail.com) 8 | */ 9 | 10 | trait PersistenceFactory { 11 | /** Indicates if an entity can be saved. */ 12 | def canSave: Boolean 13 | 14 | /** Indicates if an entity can be deleted. */ 15 | def canDelete: Boolean = canSave 16 | 17 | /** Indicates if an entity can be listed. */ 18 | def canList: Boolean = true 19 | 20 | /** Instantiates a data buffer which can be saved by EntityPersistence. 21 | * The EntityType must support copying into this object. 22 | */ 23 | def newWritable: AnyRef 24 | 25 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext): CrudPersistence 26 | 27 | /** Returns true if the URI is worth calling EntityPersistence.find to try to get an entity instance. 28 | * It may be overridden in cases where an entity instance can be found even if no ID is present in the URI. 29 | */ 30 | def maySpecifyEntityInstance(entityType: EntityType, uri: UriPath): Boolean = 31 | entityType.IdField.getter(uri).isDefined 32 | } 33 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/PersistenceOperation.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.{Operation, ActivityWithVars} 4 | import common.UriPath 5 | import persistence.EntityType 6 | 7 | /** An operation that interacts with an entity's persistence. 8 | * The CrudContext is available as persistence.crudContext to implementing classes. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | abstract class PersistenceOperation(entityType: EntityType, val application: CrudApplication) extends Operation { 12 | def invoke(uri: UriPath, persistence: CrudPersistence) 13 | 14 | def invoke(uri: UriPath, activity: ActivityWithVars) { 15 | application.withEntityPersistence(entityType, activity) { persistence => invoke(uri, persistence) } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/SQLiteEntityPersistence.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import android.provider.BaseColumns 4 | import android.database.Cursor 5 | import android.content.ContentValues 6 | import common.Common 7 | import common.PlatformTypes._ 8 | import persistence._ 9 | import scala.None 10 | import collection.mutable.SynchronizedQueue 11 | import android.app.backup.BackupManager 12 | import android.database.sqlite.{SQLiteOpenHelper, SQLiteDatabase} 13 | import common.UriPath 14 | import com.github.triangle.{PortableField, Logging} 15 | 16 | /** EntityPersistence for SQLite. 17 | * @author Eric Pabst (epabst@gmail.com) 18 | */ 19 | class SQLiteEntityPersistence(val entityType: EntityType, val crudContext: CrudContext) 20 | extends CrudPersistence with Logging { 21 | 22 | lazy val tableName = SQLitePersistenceFactory.toTableName(entityType.entityName) 23 | lazy val databaseSetup = new GeneratedDatabaseSetup(crudContext) 24 | lazy val database: SQLiteDatabase = databaseSetup.getWritableDatabase 25 | lazy val entityTypePersistedInfo = EntityTypePersistedInfo(entityType) 26 | private lazy val backupManager = new BackupManager(crudContext.context) 27 | private val cursors = new SynchronizedQueue[Cursor] 28 | private def toOption(string: String): Option[String] = if (string == "") None else Some(string) 29 | 30 | def findAll(criteria: SQLiteCriteria): CursorStream = { 31 | import entityTypePersistedInfo._ 32 | val query = criteria.selection.mkString(" AND ") 33 | info("Finding each " + this.entityType.entityName + "'s " + queryFieldNames.mkString(", ") + " where " + query + criteria.orderBy.map(" order by " + _).getOrElse("")) 34 | val cursor = database.query(tableName, queryFieldNames.toArray, 35 | toOption(query).getOrElse(null), criteria.selectionArgs.toArray, 36 | criteria.groupBy.getOrElse(null), criteria.having.getOrElse(null), criteria.orderBy.getOrElse(null)) 37 | cursors += cursor 38 | CursorStream(cursor, entityTypePersistedInfo) 39 | } 40 | 41 | //UseDefaults is provided here in the item list for the sake of PortableField.adjustment[SQLiteCriteria] fields 42 | def findAll(uri: UriPath): CursorStream = 43 | // The default orderBy is Some("_id desc") 44 | findAll(entityType.copyAndTransformWithItem(List(uri, PortableField.UseDefaults), new SQLiteCriteria(orderBy = Some(CursorField.idFieldName + " desc")))) 45 | 46 | private def notifyDataChanged() { 47 | backupManager.dataChanged() 48 | debug("Notified BackupManager that data changed.") 49 | } 50 | 51 | def newWritable = SQLitePersistenceFactory.newWritable 52 | 53 | def doSave(idOption: Option[ID], writable: AnyRef): ID = { 54 | val contentValues = writable.asInstanceOf[ContentValues] 55 | val id = idOption match { 56 | case None => { 57 | val newId = database.insertOrThrow(tableName, null, contentValues) 58 | info("Added " + entityType.entityName + " #" + newId + " with " + contentValues) 59 | newId 60 | } 61 | case Some(givenId) => { 62 | info("Updating " + entityType.entityName + " #" + givenId + " with " + contentValues) 63 | val rowCount = database.update(tableName, contentValues, BaseColumns._ID + "=" + givenId, null) 64 | if (rowCount == 0) { 65 | contentValues.put(BaseColumns._ID, givenId) 66 | info("Added " + entityType.entityName + " #" + givenId + " with " + contentValues + " since id is not present yet") 67 | val resultingId = database.insert(tableName, null, contentValues) 68 | if (givenId != resultingId) 69 | throw new IllegalStateException("id changed from " + givenId + " to " + resultingId + 70 | " when restoring " + entityType.entityName + " #" + givenId + " with " + contentValues) 71 | } 72 | givenId 73 | } 74 | } 75 | notifyDataChanged() 76 | val map = entityType.copyAndTransform(contentValues, Map[String,Any]()) 77 | val bytes = CrudBackupAgent.marshall(map) 78 | debug("Scheduled backup which will include " + entityType.entityName + "#" + id + ": size " + bytes.size + " bytes") 79 | id 80 | } 81 | 82 | def doDelete(uri: UriPath) { 83 | val ids = findAll(uri).map { readable => 84 | val id = entityType.IdField(readable) 85 | database.delete(tableName, BaseColumns._ID + "=" + id, Nil.toArray) 86 | id 87 | } 88 | future { 89 | ids.foreach { id => 90 | DeletedEntityIdCrudType.recordDeletion(entityType, id, crudContext.context) 91 | } 92 | notifyDataChanged() 93 | } 94 | } 95 | 96 | def close() { 97 | cursors.map(_.close()) 98 | database.close() 99 | } 100 | } 101 | 102 | class GeneratedDatabaseSetup(crudContext: CrudContext) 103 | extends SQLiteOpenHelper(crudContext.context, crudContext.application.nameId, null, crudContext.application.dataVersion) with Logging { 104 | 105 | protected lazy val logTag = Common.tryToEvaluate(crudContext.application.logTag).getOrElse(Common.logTag) 106 | 107 | private def createMissingTables(db: SQLiteDatabase) { 108 | val application = crudContext.application 109 | for (entityType <- application.allCrudTypes.collect { 110 | case c if c.persistenceFactory.canSave => c 111 | }.map(_.entityType)) { 112 | val buffer = new StringBuffer 113 | buffer.append("CREATE TABLE IF NOT EXISTS ").append(SQLitePersistenceFactory.toTableName(entityType.entityName)).append(" ("). 114 | append(BaseColumns._ID).append(" INTEGER PRIMARY KEY AUTOINCREMENT") 115 | EntityTypePersistedInfo(entityType).persistedFields.filter(_.columnName != BaseColumns._ID).foreach { persisted => 116 | buffer.append(", ").append(persisted.columnName).append(" ").append(persisted.persistedType.sqliteType) 117 | } 118 | buffer.append(")") 119 | execSQL(db, buffer.toString) 120 | } 121 | } 122 | 123 | def onCreate(db: SQLiteDatabase) { 124 | createMissingTables(db) 125 | } 126 | 127 | private def execSQL(db: SQLiteDatabase, sql: String) { 128 | debug("execSQL: " + sql) 129 | db.execSQL(sql) 130 | } 131 | 132 | def onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 133 | // Steps to upgrade the database for the new version ... 134 | createMissingTables(db) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/SQLitePersistenceFactory.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import android.content.ContentValues 4 | import persistence.{SQLiteUtil, EntityType} 5 | 6 | /** A PersistenceFactory for SQLite. 7 | * @author Eric Pabst (epabst@gmail.com) 8 | */ 9 | object SQLitePersistenceFactory extends PersistenceFactory { 10 | def canSave = true 11 | 12 | def newWritable = new ContentValues 13 | 14 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext) = new SQLiteEntityPersistence(entityType, crudContext) 15 | 16 | def toTableName(entityName: String): String = SQLiteUtil.toNonReservedWord(entityName) 17 | } 18 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/action/ContextVars.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.action 2 | 3 | import collection.mutable.ConcurrentMap 4 | import java.util.concurrent.ConcurrentHashMap 5 | import android.content.Context 6 | import collection.JavaConversions._ 7 | import android.app.Activity 8 | import com.github.scala.android.crud.DestroyContextListener 9 | import com.github.scala.android.crud.common.ListenerHolder 10 | 11 | /** A container for values of [[com.github.scala.android.crud.action.ContextVar]]'s */ 12 | trait ContextVars extends ListenerHolder[DestroyContextListener] { 13 | //for some reason, making this lazy results in it being null during testing, even though lazy would be preferrable. 14 | private[crud] val variables: ConcurrentMap[ContextVar[_], Any] = new ConcurrentHashMap[ContextVar[_], Any]() 15 | 16 | def onDestroyContext() { 17 | listeners.foreach(_.onDestroyContext()) 18 | } 19 | } 20 | 21 | /** A variable stored in a [[com.github.scala.android.crud.action.ContextWithVars]]. 22 | *

23 | * Normally you create an object that extends this: 24 | * {{{object ProductName extends ContextVar[String]}}} 25 | * But if you need uniqueness by instance, do this: 26 | * {{{val productName = new ContextVar[String]}}} 27 | * It doesn't accumulate any data and is sharable across threads since all data is stored in each CrudContext. 28 | */ 29 | class ContextVar[T] { 30 | /** Gets the value or None if not set. 31 | * @param context the Context where the value is stored 32 | * @return Some(value) if set, otherwise None 33 | */ 34 | def get(context: ContextVars): Option[T] = { 35 | context.variables.get(this).map(_.asInstanceOf[T]) 36 | } 37 | 38 | /** Tries to set the value in {{{crudContext}}}. 39 | * @param context the Context where the value is stored 40 | * @param value the value to set in the Context. 41 | */ 42 | def set(context: ContextVars, value: T) { 43 | context.variables.put(this, value) 44 | } 45 | 46 | def clear(context: ContextVars): Option[T] = { 47 | context.variables.remove(this).map(_.asInstanceOf[T]) 48 | } 49 | 50 | def getOrSet(context: ContextVars, initialValue: => T): T = { 51 | get(context).getOrElse { 52 | val value = initialValue 53 | set(context, value) 54 | value 55 | } 56 | } 57 | } 58 | 59 | /** Similar to ContextVar but allows specifying an initial value, evaluated when first accessed. */ 60 | class InitializedContextVar[T](initialValue: => T) { 61 | private val contextVar = new ContextVar[T] 62 | 63 | /** Gets the value or None if not set. 64 | * @param context the Context where the value is stored 65 | * @return Some(value) if set, otherwise None 66 | */ 67 | def get(context: ContextVars): T = { 68 | contextVar.getOrSet(context, initialValue) 69 | } 70 | } 71 | 72 | /** A ContextVars that has been mixed with a Context. 73 | * @author Eric Pabst (epabst@gmail.com) 74 | */ 75 | trait ContextWithVars extends Context with ContextVars 76 | 77 | /** A ContextVars that has been mixed with an Activity. */ 78 | trait ActivityWithVars extends Activity with ContextWithVars 79 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/action/Operation.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.action 2 | 3 | import android.app.Activity 4 | import com.github.scala.android.crud.common.PlatformTypes._ 5 | import android.content.{Context, Intent} 6 | import com.github.scala.android.crud.common.UriPath 7 | import com.github.scala.android.crud.view.AndroidConversions._ 8 | import android.view.View 9 | import com.github.triangle.Field 10 | import com.github.triangle.PortableField._ 11 | import com.github.scala.android.crud.view.ViewRef 12 | 13 | /** Represents something that a user can initiate. 14 | * @author Eric Pabst (epabst@gmail.com) 15 | * @param icon The optional icon to display. 16 | * @param title The title to display. 17 | * @param viewRef The ViewKey (or equivalent) that represents the Command for the user to click on. Optional. 18 | * If the title is None, it can't be displayed in a context menu for a list item. 19 | * If both title and icon are None, 20 | * then it can't be displayed in the main options menu, but can still be triggered as a default. 21 | */ 22 | case class Command(icon: Option[ImgKey], title: Option[SKey], viewRef: Option[ViewRef] = None) { 23 | /** A CommandID that can be used to identify if it's the same as another in a list. 24 | * It uses the title or else the icon or else the hash code. 25 | */ 26 | def commandId: CommandId = title.orElse(icon).getOrElse(##) 27 | } 28 | 29 | /** Represents an operation that a user can initiate. */ 30 | trait Operation { 31 | /** Runs the operation, given the uri and the current state of the application. */ 32 | def invoke(uri: UriPath, activity: ActivityWithVars) 33 | } 34 | 35 | object Operation { 36 | val CreateActionName = Intent.ACTION_INSERT 37 | val ListActionName = Intent.ACTION_PICK 38 | val DisplayActionName = Intent.ACTION_VIEW 39 | val UpdateActionName = Intent.ACTION_EDIT 40 | val DeleteActionName = Intent.ACTION_DELETE 41 | 42 | implicit def toRichItent(intent: Intent) = new RichIntent(intent) 43 | 44 | //this is a workaround because Robolectric doesn't handle the full constructor 45 | def constructIntent(action: String, uriPath: UriPath, context: Context, clazz: Class[_]): Intent = { 46 | val intent = new Intent(action, uriPath) 47 | intent.setClass(context, clazz) 48 | intent 49 | } 50 | } 51 | 52 | /** Represents an action that a user can initiate. 53 | * It's equals/hashCode MUST be implemented in order to suppress the action that is already happening. 54 | */ 55 | case class Action(command: Command, operation: Operation) { 56 | def commandId: CommandId = command.commandId 57 | 58 | def invoke(uri: UriPath, activity: ActivityWithVars) { 59 | operation.invoke(uri, activity) 60 | } 61 | } 62 | 63 | case class RichIntent(intent: Intent) { 64 | def uriPath: UriPath = intent.getData 65 | } 66 | 67 | trait StartActivityOperation extends Operation { 68 | def determineIntent(uri: UriPath, activity: ActivityWithVars): Intent 69 | 70 | def invoke(uri: UriPath, activity: ActivityWithVars) { 71 | activity.startActivity(determineIntent(uri, activity)) 72 | } 73 | } 74 | 75 | trait BaseStartActivityOperation extends StartActivityOperation { 76 | def action: String 77 | 78 | def activityClass: Class[_ <: Activity] 79 | 80 | def determineIntent(uri: UriPath, activity: ActivityWithVars): Intent = Operation.constructIntent(action, uri, activity, activityClass) 81 | } 82 | 83 | /** An Operation that starts an Activity using the provided Intent. 84 | * @param intent the Intent to use to start the Activity. It is pass-by-name because the SDK's Intent has a "Stub!" error. */ 85 | class StartActivityOperationFromIntent(intent: => Intent) extends StartActivityOperation { 86 | def determineIntent(uri: UriPath, activity: ActivityWithVars) = intent 87 | } 88 | 89 | //final to guarantee equality is correct 90 | final case class StartNamedActivityOperation(action: String, activityClass: Class[_ <: Activity]) extends BaseStartActivityOperation 91 | 92 | trait EntityOperation extends Operation { 93 | def entityName: String 94 | def action: String 95 | } 96 | 97 | //final to guarantee equality is correct 98 | final case class StartEntityActivityOperation(entityName: String, action: String, activityClass: Class[_ <: Activity]) 99 | extends BaseStartActivityOperation with EntityOperation { 100 | 101 | override def determineIntent(uri: UriPath, activity: ActivityWithVars): Intent = 102 | super.determineIntent(uri.specify(entityName), activity) 103 | } 104 | 105 | //final to guarantee equality is correct 106 | final case class StartEntityIdActivityOperation(entityName: String, action: String, activityClass: Class[_ <: Activity]) 107 | extends BaseStartActivityOperation with EntityOperation { 108 | 109 | override def determineIntent(uri: UriPath, activity: ActivityWithVars) = super.determineIntent(uri.upToIdOf(entityName), activity) 110 | } 111 | 112 | trait StartActivityForResultOperation extends StartActivityOperation { 113 | def viewIdToRespondTo: ViewKey 114 | 115 | override def invoke(uri: UriPath, activity: ActivityWithVars) { 116 | activity.startActivityForResult(determineIntent(uri, activity), viewIdToRespondTo) 117 | } 118 | } 119 | 120 | object StartActivityForResultOperation { 121 | def apply(view: View, intent: => Intent): StartActivityForResultOperation = 122 | new StartActivityOperationFromIntent(intent) with StartActivityForResultOperation { 123 | def viewIdToRespondTo = view.getId 124 | } 125 | } 126 | 127 | /** The response to a [[com.github.scala.android.crud.action.StartActivityForResultOperation]]. 128 | * This is used by [[com.github.scala.android.crud.CrudActivity]]'s startActivityForResult. 129 | */ 130 | case class OperationResponse(viewIdRespondingTo: ViewKey, intent: Intent) 131 | 132 | /** An extractor to get the OperationResponse from the items being copied from. */ 133 | object OperationResponseExtractor extends Field(identityField[OperationResponse]) 134 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/action/OptionsMenuActivity.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.action 2 | 3 | import android.view.Menu 4 | import java.lang.reflect.Method 5 | import java.util.concurrent.atomic.AtomicBoolean 6 | 7 | /** An Activity that has an options menu. 8 | * This is intended to handle both Android 2 and 3. 9 | * The options menu in Android 3 can be left visible all the time until invalidated. 10 | * When the options menu changes, invoke {{{this.optionsMenuCommands = ...}}} 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | trait OptionsMenuActivity extends ActivityWithVars { 14 | protected def initialOptionsMenuCommands: List[Command] 15 | 16 | // Use a ContextVar to make it thread-safe 17 | private object OptionsMenuCommandsVar extends ContextVar[List[Command]] 18 | 19 | final def optionsMenuCommands: List[Command] = OptionsMenuCommandsVar.get(this).getOrElse(initialOptionsMenuCommands) 20 | 21 | def optionsMenuCommands_=(newValue: List[Command]) { 22 | OptionsMenuCommandsVar.set(this, newValue) 23 | invalidateOptionsMenuMethod.map(_.invoke(this)).getOrElse(recreateInPrepare.set(true)) 24 | } 25 | 26 | private val recreateInPrepare = new AtomicBoolean(false) 27 | private lazy val invalidateOptionsMenuMethod: Option[Method] = 28 | try { Option(getClass.getMethod("invalidateOptionsMenu"))} 29 | catch { case _ => None } 30 | 31 | private[action] def populateMenu(menu: Menu, commands: List[Command]) { 32 | for ((command, index) <- commands.zip(Stream.from(0))) { 33 | val menuItem = command.title.map(menu.add(0, command.commandId, index, _)).getOrElse(menu.add(0, command.commandId, index, "")) 34 | command.icon.map(icon => menuItem.setIcon(icon)) 35 | } 36 | } 37 | 38 | override def onCreateOptionsMenu(menu: Menu): Boolean = { 39 | populateMenu(menu, optionsMenuCommands) 40 | true 41 | } 42 | 43 | override def onPrepareOptionsMenu(menu: Menu) = { 44 | if (recreateInPrepare.getAndSet(false)) { 45 | menu.clear() 46 | populateMenu(menu, optionsMenuCommands) 47 | true 48 | } else { 49 | super.onPrepareOptionsMenu(menu) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/CachedFunction.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | import scala.collection.JavaConversions._ 5 | import collection.mutable.ConcurrentMap 6 | 7 | /** A cache of results of a function. 8 | * @author Eric Pabst (epabst@gmail.com) 9 | */ 10 | case class CachedFunction[A,B](function: (A) => B) extends ((A) => B) { 11 | private val resultByInput: ConcurrentMap[A,B] = new ConcurrentHashMap[A,B]() 12 | 13 | def apply(input: A): B = resultByInput.get(input).getOrElse { 14 | val result = function(input) 15 | resultByInput.putIfAbsent(input, result) 16 | result 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/CalculatedIterator.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | /** An Iterator whose items are calculated lazily. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | private[crud] trait CalculatedIterator[T] extends BufferedIterator[T] { 7 | private var calculatedNextValue: Option[Option[T]] = None 8 | 9 | def calculateNextValue(): Option[T] 10 | 11 | private def determineNextValue(): Option[T] = { 12 | if (!calculatedNextValue.isDefined) { 13 | calculatedNextValue = Some(calculateNextValue()) 14 | } 15 | calculatedNextValue.get 16 | } 17 | 18 | def hasNext = determineNextValue().isDefined 19 | 20 | def head = determineNextValue().get 21 | 22 | def next() = { 23 | val next = head 24 | calculatedNextValue = None 25 | next 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/Common.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | /** Common functionality that are use throughout scala-android-crud. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | 7 | object Common { 8 | val logTag = "scala-android-crud" 9 | 10 | /** Evaluates the given function and returns the result. If it throws an exception, it returns None. */ 11 | def tryToEvaluate[T](f: => T): Option[T] = { 12 | try { Some(f) } 13 | catch { case _ => None } 14 | } 15 | 16 | def withCloseable[C <: {def close()},T](closeable: C)(f: C => T): T = { 17 | try { f(closeable) } 18 | finally { closeable.close() } 19 | } 20 | } -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/ListenerHolder.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | import collection.mutable.ConcurrentMap 5 | import scala.collection.JavaConversions._ 6 | import scala.collection.Set 7 | 8 | /** A Listener holder 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | 12 | trait ListenerHolder[L] { 13 | private val theListeners: ConcurrentMap[L,L] = new ConcurrentHashMap[L,L]() 14 | 15 | protected def listeners: Set[L] = theListeners.keySet 16 | 17 | def addListener(listener: L) { 18 | theListeners += listener -> listener 19 | } 20 | 21 | def removeListener(listener: L) { 22 | theListeners -= listener 23 | } 24 | } -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/PlatformTypes.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | /** A single place to specify types that could vary between different platforms. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | 7 | object PlatformTypes { 8 | /** An entity ID */ 9 | type ID = Long 10 | /** A string key used with translation. */ 11 | type SKey = Int 12 | /** An image key used with translation. */ 13 | type ImgKey = Int 14 | /** A layout key. */ 15 | type LayoutKey = Int 16 | /** A view key, which is a single element of a layout. */ 17 | type ViewKey = Int 18 | /** An ID for a command. */ 19 | type CommandId = Int 20 | } -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/ReadyFuture.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import actors.{InputChannel, Future} 4 | 5 | /** A Future that is ready. 6 | * @author Eric Pabst (epabst@gmail.com) 7 | */ 8 | class ReadyFuture[+T](val readyValue: T) extends Future[T] { 9 | def isSet = true 10 | 11 | def apply() = readyValue 12 | 13 | def respond(action: (T) => Unit) { action(readyValue) } 14 | 15 | lazy val inputChannel = new InputChannel[T] { 16 | def ? = readyValue 17 | 18 | def react(f: PartialFunction[T, Unit]): Nothing = { f(readyValue); throw new IllegalStateException("this method can't return") } 19 | 20 | def reactWithin(msec: Long)(f: PartialFunction[Any, Unit]): Nothing = { react(f) } 21 | 22 | def receive[R](f: PartialFunction[T, R]) = f(readyValue) 23 | 24 | def receiveWithin[R](msec: Long)(f: PartialFunction[Any, R]) = receive(f) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/Timing.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import actors.Futures 4 | import android.view.View 5 | import android.app.Activity 6 | import com.github.triangle.Logging 7 | 8 | /** A utility for interacting with threads, which enables overriding for testing. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | 12 | trait Timing extends Logging { 13 | private def withExceptionLogging[T](body: => T): T = { 14 | try { 15 | body 16 | } catch { 17 | case e => 18 | logError("Error in non-UI Thread", e) 19 | throw e 20 | } 21 | } 22 | 23 | def future[T](body: => T): scala.actors.Future[T] = { 24 | Futures.future(withExceptionLogging(body)) 25 | } 26 | 27 | def runOnUiThread[T](view: View)(body: => T) { 28 | view.post(toRunnable(withExceptionLogging(body))) 29 | } 30 | 31 | def runOnUiThread[T](activity: Activity)(body: => T) { 32 | activity.runOnUiThread(toRunnable(withExceptionLogging(body))) 33 | } 34 | 35 | private def toRunnable(operation: => Unit): Runnable = new Runnable { 36 | def run() { 37 | operation 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/common/UriPath.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import PlatformTypes._ 4 | import com.github.triangle.ValueFormat._ 5 | import com.github.triangle.{Getter, FieldGetter} 6 | 7 | /** A convenience wrapper for UriPath. 8 | * It helps in that UriPath.EMPTY is null when running unit tests, and helps prepare for multi-platform support. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | case class UriPath(segments: String*) { 12 | private lazy val idFormat = basicFormat[ID] 13 | 14 | def /(segment: String): UriPath = UriPath(segments :+ segment:_*) 15 | 16 | def /(id: ID): UriPath = this / idFormat.toString(id) 17 | 18 | def specify(finalSegments: String*): UriPath = 19 | UriPath.replacePathSegments(this, _.takeWhile(_ != finalSegments.head) ++ finalSegments.toList) 20 | 21 | def lastEntityNameOption: Option[String] = segments.reverse.find(idFormat.toValue(_).isEmpty) 22 | 23 | def findId(entityName: String): Option[ID] = 24 | segments.dropWhile(_ != entityName).toList match { 25 | case _ :: idString :: tail => idFormat.toValue(idString) 26 | case _ => None 27 | } 28 | 29 | def upToIdOf(entityName: String): UriPath = specify(entityName +: findId(entityName).map(_.toString).toList:_*) 30 | 31 | override def toString = segments.mkString("/", "/", "") 32 | } 33 | 34 | object UriPath { 35 | val EMPTY: UriPath = UriPath() 36 | 37 | private def toOption(string: String): Option[String] = if (string == "") None else Some(string) 38 | 39 | def apply(string: String): UriPath = UriPath(toOption(string.stripPrefix("/")).map(_.split("/").toSeq).getOrElse(Nil):_*) 40 | 41 | private[UriPath] def replacePathSegments(uri: UriPath, f: Seq[String] => Seq[String]): UriPath = { 42 | val path = f(uri.segments) 43 | UriPath(path: _*) 44 | } 45 | 46 | def uriIdField(entityName: String): FieldGetter[UriPath,ID] = Getter[UriPath,ID](_.findId(entityName)) 47 | } 48 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/generate/EntityFieldInfo.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.generate 2 | 3 | import com.github.scala.android.crud.view.AndroidResourceAnalyzer._ 4 | import com.github.triangle.{PortableField, FieldList, BaseField} 5 | import com.github.scala.android.crud.persistence.{EntityType, CursorField} 6 | import com.github.scala.android.crud.view._ 7 | import xml.NodeSeq 8 | import com.github.scala.android.crud.NamingConventions 9 | 10 | case class EntityFieldInfo(field: BaseField, rIdClasses: Seq[Class[_]]) { 11 | private lazy val updateablePersistedFields = CursorField.updateablePersistedFields(field, rIdClasses) 12 | 13 | private def viewFields(field: BaseField): List[ViewField[_]] = field.deepCollect { 14 | case matchingField: ViewField[_] => matchingField 15 | } 16 | 17 | lazy val viewIdFieldInfos: List[ViewIdFieldInfo] = field.deepCollect { 18 | case viewIdField: ViewIdField[_] => ViewIdFieldInfo(viewIdField.viewRef.fieldName(rIdClasses), viewIdField) 19 | } 20 | 21 | lazy val isDisplayable: Boolean = !displayableViewIdFieldInfos.isEmpty 22 | def isPersisted: Boolean = !updateablePersistedFields.isEmpty 23 | def isUpdateable: Boolean = isDisplayable && isPersisted 24 | 25 | lazy val nestedEntityTypeViewInfos: List[EntityTypeViewInfo] = field.deepCollect { 26 | case entityView: EntityView => EntityTypeViewInfo(entityView.entityType) 27 | } 28 | 29 | lazy val displayableViewIdFieldInfos: List[ViewIdFieldInfo] = 30 | viewIdFieldInfos.filter(_.layout.displayXml != NodeSeq.Empty) ++ nestedEntityTypeViewInfos.flatMap(_.displayableViewIdFieldInfos) 31 | 32 | lazy val updateableViewIdFieldInfos: List[ViewIdFieldInfo] = 33 | if (isPersisted) viewIdFieldInfos.filter(_.layout.editXml != NodeSeq.Empty) else Nil 34 | 35 | lazy val otherViewFields = { 36 | val viewFieldsWithinViewIdFields = viewFields(FieldList(viewIdFieldInfos.map(_.field):_*)) 37 | viewFields(field).filterNot(viewFieldsWithinViewIdFields.contains) 38 | } 39 | } 40 | 41 | case class ViewIdFieldInfo(id: String, displayName: String, field: PortableField[_]) { 42 | lazy val viewFields: List[ViewField[_]] = field.deepCollect { 43 | case matchingField: ViewField[_] => matchingField 44 | } 45 | 46 | def layout: FieldLayout = viewFields.headOption.map(_.defaultLayout).getOrElse(FieldLayout.noLayout) 47 | } 48 | 49 | object ViewIdFieldInfo { 50 | def apply(id: String, viewField: PortableField[_]): ViewIdFieldInfo = 51 | ViewIdFieldInfo(id, FieldLayout.toDisplayName(id), viewField) 52 | } 53 | 54 | case class EntityTypeViewInfo(entityType: EntityType) { 55 | def entityName = entityType.entityName 56 | lazy val layoutPrefix = NamingConventions.toLayoutPrefix(entityType.entityName) 57 | lazy val rIdClasses: Seq[Class[_]] = detectRIdClasses(entityType.getClass) 58 | lazy val entityFieldInfos: List[EntityFieldInfo] = entityType.fields.map(EntityFieldInfo(_, rIdClasses)) 59 | lazy val displayableViewIdFieldInfos: List[ViewIdFieldInfo] = entityFieldInfos.flatMap(_.displayableViewIdFieldInfos) 60 | lazy val updateableViewIdFieldInfos: List[ViewIdFieldInfo] = entityFieldInfos.flatMap(_.updateableViewIdFieldInfos) 61 | def isUpdateable: Boolean = !updateableViewIdFieldInfos.isEmpty 62 | } 63 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/CursorField.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import android.content.ContentValues 4 | import android.database.Cursor 5 | import android.provider.BaseColumns 6 | import com.github.triangle._ 7 | import PortableField._ 8 | import android.os.Bundle 9 | import com.github.scala.android.crud.common.PlatformTypes._ 10 | import com.github.scala.android.crud.ParentField 11 | import com.github.triangle.Converter._ 12 | import android.net.Uri 13 | 14 | case class SQLiteCriteria(selection: List[String] = Nil, selectionArgs: List[String] = Nil, 15 | groupBy: Option[String] = None, having: Option[String] = None, orderBy: Option[String] = None) 16 | 17 | object CursorField { 18 | def bundleField[T](name: String)(implicit persistedType: PersistedType[T]) = 19 | Getter[Bundle,T](b => persistedType.getValue(b, name)).withSetter(b => v => persistedType.putValue(b, name, v), noSetterForEmpty) + 20 | mapField[T](name) 21 | 22 | def persisted[T](name: String)(implicit persistedType: PersistedType[T]): CursorField[T] = { 23 | new CursorField[T](name)(persistedType) 24 | } 25 | 26 | def persistedUri(name: String): PortableField[Uri] = 27 | converted[Uri,String](anyToString, persisted(name), (s: String) => Some(Uri.parse(s))) 28 | 29 | def persistedEnum[E <: Enumeration#Value](name: String, enumeration: Enumeration)(implicit m: Manifest[E]): PortableField[E] = 30 | formatted[E](ValueFormat.enumFormat(enumeration), persisted(name)) 31 | 32 | def persistedDate(name: String) = converted(dateToLong, persisted[Long](name), longToDate) 33 | 34 | def persistedCalendar(name: String) = converted(calendarToDate, persistedDate(name), dateToCalendar) 35 | 36 | val idFieldName = BaseColumns._ID 37 | 38 | object PersistedId extends Field[ID](persisted[ID](idFieldName) + sqliteCriteria(idFieldName)) 39 | 40 | def persistedFields(field: BaseField): List[CursorField[_]] = { 41 | field.deepCollect[CursorField[_]] { 42 | case cursorField: CursorField[_] => cursorField 43 | } 44 | } 45 | 46 | def updateablePersistedFields(field: BaseField, rIdClasses: Seq[Class[_]]): List[CursorField[_]] = { 47 | val parentFieldNames = ParentField.parentFields(field).map(_.fieldName) 48 | persistedFields(field).filterNot(_.name == idFieldName).filterNot(parentFieldNames.contains(_)) 49 | } 50 | 51 | def queryFieldNames(fields: FieldList): List[String] = persistedFields(fields).map(_.columnName) 52 | 53 | def sqliteCriteria[T](name: String): PortableField[T] = 54 | Transformer((criteria: SQLiteCriteria) => (v: T) => { 55 | val formattedValue = v match { 56 | case s: String => "\"" + s + "\"" 57 | case n => n.toString 58 | } 59 | criteria.copy(selection = (name + "=" + formattedValue) +: criteria.selection) 60 | }, noTransformerForEmpty[SQLiteCriteria]) 61 | 62 | } 63 | 64 | import CursorField._ 65 | 66 | /** Also supports accessing a scala Map (mutable.Map for writing) using the same name. */ 67 | class CursorField[T](val name: String)(implicit val persistedType: PersistedType[T]) extends DelegatingPortableField[T] with Logging { 68 | protected val delegate = Getter[Cursor,T](getFromCursor) + 69 | Setter((c: ContentValues) => (value: T) => persistedType.putValue(c, columnName, value), noSetterForEmpty) + 70 | bundleField[T](name) 71 | 72 | lazy val columnName = SQLiteUtil.toNonReservedWord(name) 73 | 74 | private def getFromCursor(cursor: Cursor) = { 75 | val columnIndex = cursor.getColumnIndex(columnName) 76 | if (columnIndex >= 0) { 77 | persistedType.getValue(cursor, columnIndex) 78 | } else { 79 | warn("column not in Cursor: " + columnName) 80 | None 81 | } 82 | } 83 | 84 | override def toString = "persisted(\"" + name + "\")" 85 | } 86 | 87 | object SQLiteUtil { 88 | def toNonReservedWord(name: String): String = name.toUpperCase match { 89 | case "ABORT" | "ACTION" | "ADD" | "AFTER" | "ALL" | "ALTER" | "ANALYZE" | "AND" | "AS" | "ASC" | "ATTACH" | 90 | "AUTOINCREMENT" | "BEFORE" | "BEGIN" | "BETWEEN" | "BY" | "CASCADE" | "CASE" | "CAST" | "CHECK" | 91 | "COLLATE" | "COLUMN" | "COMMIT" | "CONFLICT" | "CONSTRAINT" | "CREATE" | "CROSS" | "CURRENT_DATE" | 92 | "CURRENT_TIME" | "CURRENT_TIMESTAMP" | "DATABASE" | "DEFAULT" | "DEFERRABLE" | "DEFERRED" | "DELETE" | 93 | "DESC" | "DETACH" | "DISTINCT" | "DROP" | "EACH" | "ELSE" | "END" | "ESCAPE" | "EXCEPT" | "EXCLUSIVE" | 94 | "EXISTS" | "EXPLAIN" | "FAIL" | "FOR" | "FOREIGN" | "FROM" | "FULL" | "GLOB" | "GROUP" | "HAVING" | 95 | "IF" | "IGNORE" | "IMMEDIATE" | "IN" | "INDEX" | "INDEXED" | "INITIALLY" | "INNER" | "INSERT" | "INSTEAD" | 96 | "INTERSECT" | "INTO" | "IS" | "ISNULL" | "JOIN" | "KEY" | "LEFT" | "LIKE" | "LIMIT" | "MATCH" | "NATURAL" | 97 | "NO" | "NOT" | "NOTNULL" | "NULL" | "OF" | "OFFSET" | "ON" | "OR" | "ORDER" | "OUTER" | "PLAN" | 98 | "PRAGMA" | "PRIMARY" | "QUERY" | "RAISE" | "REFERENCES" | "REGEXP" | "REINDEX" | "RELEASE" | "RENAME" | 99 | "REPLACE" | "RESTRICT" | "RIGHT" | "ROLLBACK" | "ROW" | "SAVEPOINT" | "SELECT" | "SET" | "TABLE" | 100 | "TEMP" | "TEMPORARY" | "THEN" | "TO" | "TRANSACTION" | "TRIGGER" | "UNION" | "UNIQUE" | "UPDATE" | 101 | "USING" | "VACUUM" | "VALUES" | "VIEW" | "VIRTUAL" | "WHEN" | "WHERE" => name + "0" 102 | case _ => name 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/CursorStream.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import android.database.Cursor 4 | import com.github.triangle.FieldList 5 | 6 | case class EntityTypePersistedInfo(persistedFields: List[CursorField[_]]) { 7 | private val persistedFieldList = FieldList(persistedFields: _*) 8 | lazy val queryFieldNames: List[String] = persistedFields.map(_.columnName) 9 | 10 | /** Copies the current row of the given cursor to a Map. This allows the Cursor to then move to a different position right after this. */ 11 | def copyRowToMap(cursor: Cursor): Map[String,Any] = 12 | persistedFieldList.copyAndTransform(cursor, Map.empty[String,Any]) 13 | } 14 | 15 | object EntityTypePersistedInfo { 16 | def apply(entityType: EntityType): EntityTypePersistedInfo = EntityTypePersistedInfo(CursorField.persistedFields(entityType)) 17 | } 18 | 19 | /** A Stream that wraps a Cursor. 20 | * @author Eric Pabst (epabst@gmail.com) 21 | */ 22 | case class CursorStream(cursor: Cursor, entityTypePersistedInfo: EntityTypePersistedInfo) extends Stream[Map[String,Any]] { 23 | override lazy val headOption = { 24 | if (cursor.moveToNext) { 25 | Some(entityTypePersistedInfo.copyRowToMap(cursor)) 26 | } else { 27 | cursor.close() 28 | None 29 | } 30 | } 31 | 32 | override def isEmpty : scala.Boolean = headOption.isEmpty 33 | override def head = headOption.get 34 | override def length = cursor.getCount 35 | 36 | def tailDefined = !isEmpty 37 | // Must be a val so that we don't create more than one CursorStream. 38 | // Must be lazy so that we don't instantiate the entire stream 39 | override lazy val tail = if (tailDefined) CursorStream(cursor, entityTypePersistedInfo) else throw new NoSuchElementException 40 | } 41 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/EntityPersistence.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import com.github.scala.android.crud.common.PlatformTypes._ 4 | import com.github.scala.android.crud.common.{Common, ListenerHolder, Timing, UriPath} 5 | 6 | trait PersistenceListener { 7 | def onSave(id: ID) 8 | 9 | def onDelete(uri: UriPath) 10 | } 11 | 12 | /** Persistence support for an entity. 13 | * @author Eric Pabst (epabst@gmail.com) 14 | */ 15 | 16 | trait EntityPersistence extends Timing with ListenerHolder[PersistenceListener] { 17 | protected def logTag: String = Common.logTag 18 | 19 | def toUri(id: ID): UriPath 20 | 21 | /** Finds one result for a given uri. The UriPath should uniquely identify an entity. 22 | * @throws IllegalStateException if more than one entity matches the UriPath. 23 | */ 24 | def find(uri: UriPath): Option[AnyRef] = { 25 | val results = findAll(uri) 26 | if (!results.isEmpty && !results.tail.isEmpty) throw new IllegalStateException("multiple results for " + uri + ": " + results.mkString(", ")) 27 | results.headOption 28 | } 29 | 30 | def findAll(uri: UriPath): Seq[AnyRef] 31 | 32 | /** Should delegate to PersistenceFactory.newWritable. */ 33 | def newWritable: AnyRef 34 | 35 | /** Save a created or updated entity. */ 36 | final def save(idOption: Option[ID], writable: AnyRef): ID = { 37 | val id = doSave(idOption, writable) 38 | listeners.foreach(_.onSave(id)) 39 | id 40 | } 41 | 42 | def doSave(id: Option[ID], writable: AnyRef): ID 43 | 44 | /** Delete a set of entities by uri. 45 | * This should NOT delete child entities because that would make the "undo" functionality incomplete. 46 | * Instead, assume that the CrudType will handle deleting all child entities explicitly. 47 | */ 48 | final def delete(uri: UriPath) { 49 | doDelete(uri) 50 | listeners.foreach(_.onDelete(uri)) 51 | } 52 | 53 | def doDelete(uri: UriPath) 54 | 55 | def close() 56 | } 57 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/EntityType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import com.github.triangle._ 4 | import com.github.scala.android.crud.common._ 5 | import PlatformTypes._ 6 | import CursorField.PersistedId 7 | import UriPath.uriIdField 8 | 9 | /** An entity configuration that provides information needed to map data to and from persistence. 10 | * This shouldn't depend on the platform (e.g. android). 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | trait EntityType extends FieldList with Logging { 14 | override lazy val logTag = Common.tryToEvaluate(entityName).getOrElse(Common.logTag) 15 | 16 | //this is the type used for internationalized strings 17 | def entityName: String 18 | 19 | object UriPathId extends Field[ID](uriIdField(entityName)) 20 | 21 | /** This should only be used in order to override this. IdField should be used instead of this. 22 | * A field that uses IdPk.id is NOT included here because it could match a related entity that also extends IdPk, 23 | * which results in many problems. 24 | */ 25 | protected def idField: PortableField[ID] = UriPathId + PersistedId 26 | object IdField extends Field[ID](idField) 27 | 28 | /** The fields other than the primary key. */ 29 | def valueFields: List[BaseField] 30 | 31 | /** The idField along with accessors for IdPk instances. */ 32 | lazy val idPkField = IdField + Getter[IdPk,ID](_.id).withTransformer(e => e.id(_)) + 33 | Setter((e: MutableIdPk) => e.id = _) 34 | lazy val fieldsIncludingIdPk = FieldList((idPkField +: fields): _*) 35 | 36 | /** These are all of the entity's fields, which includes IdPk.idField and the valueFields. */ 37 | final lazy val fields: List[BaseField] = IdField +: valueFields 38 | 39 | def toUri(id: ID) = UriPath(entityName, id.toString) 40 | 41 | lazy val DefaultPortableValue = copyFrom(PortableField.UseDefaults) 42 | 43 | override def toString() = entityName 44 | } 45 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/IdPk.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import com.github.scala.android.crud.common.PlatformTypes._ 4 | 5 | /** A trait with a primary key 6 | * @author Eric Pabst (epabst@gmail.com) 7 | */ 8 | trait IdPk { 9 | def id: Option[ID] 10 | 11 | def id(newId: Option[ID]): IdPk 12 | } 13 | 14 | trait MutableIdPk extends IdPk { 15 | var id: Option[ID] = None 16 | 17 | def id(newId: Option[ID]) = { 18 | id = newId 19 | this 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/persistence/SeqEntityPersistence.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import scala.collection.mutable 4 | import com.github.scala.android.crud.common.UriPath 5 | import com.github.triangle.{Setter, Getter, Field} 6 | import com.github.scala.android.crud.common.PlatformTypes._ 7 | import java.util.concurrent.atomic.AtomicLong 8 | 9 | /** EntityPersistence for a simple generated Seq. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | 13 | trait SeqEntityPersistence[T <: AnyRef] extends EntityPersistence { 14 | def newWritable: T 15 | } 16 | 17 | trait ReadOnlyPersistence extends EntityPersistence { 18 | def newWritable = throw new UnsupportedOperationException("write not supported") 19 | 20 | def doSave(id: Option[ID], data: AnyRef): ID = throw new UnsupportedOperationException("write not supported") 21 | 22 | def doDelete(uri: UriPath) { throw new UnsupportedOperationException("delete not supported") } 23 | 24 | def close() {} 25 | } 26 | 27 | abstract class ListBufferEntityPersistence[T <: AnyRef](newWritableFunction: => T) extends SeqEntityPersistence[T] { 28 | private object IdField extends Field[ID](Getter[IdPk,ID](_.id).withTransformer(e => e.id(_)) + 29 | Setter((e: MutableIdPk) => e.id = _) + CursorField.PersistedId) 30 | val buffer = mutable.ListBuffer[T]() 31 | 32 | val nextId = new AtomicLong(10000L) 33 | 34 | //todo only return the one that matches the ID in the uri, if present 35 | //def findAll(uri: UriPath) = buffer.toList.filter(item => uri.segments.containsSlice(toUri(IdField(item)).segments)) 36 | def findAll(uri: UriPath) = buffer.toList 37 | 38 | def newWritable = newWritableFunction 39 | 40 | def doSave(id: Option[ID], item: AnyRef) = { 41 | val newId = id.getOrElse { 42 | nextId.incrementAndGet() 43 | } 44 | // Prepend so that the newest ones come out first in results 45 | buffer.prepend(IdField.transformer[T](item.asInstanceOf[T])(Some(newId))) 46 | newId 47 | } 48 | 49 | def doDelete(uri: UriPath) { 50 | findAll(uri).foreach(entity => buffer -= entity) 51 | } 52 | 53 | def close() {} 54 | } 55 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/validate/Validation.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.validate 2 | 3 | import com.github.triangle.Transformer 4 | import scala.{PartialFunction, AnyRef} 5 | 6 | case class ValidationResult(numInvalid: Int) { 7 | def isValid: Boolean = numInvalid == 0 8 | 9 | def +(isValid: Boolean): ValidationResult = if (isValid) this else ValidationResult(numInvalid + 1) 10 | } 11 | 12 | object ValidationResult { 13 | /** The result for valid data. It is capitalized so it can be used in case statements. */ 14 | val Valid: ValidationResult = ValidationResult(0) 15 | } 16 | 17 | /** A PortableField for validating data. It transforms a ValidationResult using a value. 18 | * @author Eric Pabst (epabst@gmail.com) 19 | */ 20 | class Validation[T](isValid: Option[T] => Boolean) extends Transformer[T] { 21 | def transformer[S <: AnyRef]: PartialFunction[S,Option[T] => S] = { 22 | case result: ValidationResult => value => (result + isValid(value)).asInstanceOf[S] 23 | } 24 | } 25 | 26 | object Validation { 27 | def apply[T](isValid: Option[T] => Boolean): Validation[T] = new Validation[T](isValid) 28 | 29 | /** A Validation that requires that the value be defined. 30 | * It does allow the value to be an empty string, empty list, etc. 31 | * Example:

field... + required
32 | */ 33 | def required[T]: Validation[T] = Validation(_.isDefined) 34 | 35 | /** A Validation that requires that the value be defined and not one of the given values. 36 | * Example:
field... + requiredAndNot("")
37 | */ 38 | def requiredAndNot[T](invalidValues: T*): Validation[T] = 39 | Validation(value => value.isDefined && !invalidValues.contains(value.get)) 40 | 41 | /** A Validation that requires that the value not be empty (after trimming). */ 42 | def requiredString: Validation[String] = Validation(_.map(s => s.trim != "").getOrElse(false)) 43 | } 44 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/AndroidConversions.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import android.view.View 4 | import android.view.View.OnClickListener 5 | import android.net.Uri 6 | import com.github.scala.android.crud.common.UriPath 7 | 8 | /** Useful conversions for Android development. */ 9 | object AndroidConversions { 10 | import scala.collection.JavaConversions._ 11 | implicit def toUriPath(uri: Uri): UriPath = UriPath(uri.getPathSegments.toList:_*) 12 | 13 | implicit def toUri(uriPath: UriPath): Uri = 14 | uriPath.segments.foldLeft(Uri.EMPTY)((uri, segment) => Uri.withAppendedPath(uri, segment)) 15 | 16 | implicit def toOnClickListener(body: View => Unit) = new OnClickListener { 17 | def onClick(view: View) { body(view) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/AndroidResourceAnalyzer.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import java.lang.reflect.{Modifier, Field} 4 | import com.github.triangle.Logging 5 | import com.github.scala.android.crud.common.Common 6 | 7 | /** An "R" analyzer. 8 | * @author Eric Pabst (epabst@gmail.com) 9 | */ 10 | 11 | object AndroidResourceAnalyzer extends Logging { 12 | protected def logTag = Common.logTag 13 | 14 | private def findRInnerClass(classInSamePackage: Class[_], innerClassName: String): Option[Class[_]] = { 15 | findRInnerClass(classInSamePackage.getClassLoader, classInSamePackage.getPackage.getName, innerClassName) 16 | } 17 | 18 | private def findRInnerClass(classLoader: ClassLoader, packageName: String, innerClassName: String): Option[Class[_]] = { 19 | try { Some(classLoader.loadClass(packageName + ".R$" + innerClassName)) } 20 | catch { case e: ClassNotFoundException => 21 | val parentPackagePieces = packageName.split('.').dropRight(1) 22 | if (parentPackagePieces.isEmpty) None 23 | else findRInnerClass(classLoader, parentPackagePieces.mkString("."), innerClassName) 24 | } 25 | } 26 | 27 | private def findMatchingResourceField(classes: Seq[Class[_]], matcher: Field => Boolean): Option[Field] = { 28 | classes.view.flatMap(_.getDeclaredFields.find { field => 29 | Modifier.isStatic(field.getModifiers) && matcher(field) 30 | }).headOption 31 | } 32 | 33 | def detectRIdClasses(clazz: Class[_]): Seq[Class[_]] = { 34 | findRInnerClass(clazz, "id").toSeq ++ Seq(classOf[android.R.id], classOf[com.github.scala.android.crud.res.R.id]) 35 | } 36 | 37 | def detectRLayoutClasses(clazz: Class[_]): Seq[Class[_]] = { 38 | findRInnerClass(clazz, "layout").toSeq ++ Seq(classOf[android.R.layout], classOf[com.github.scala.android.crud.res.R.layout]) 39 | } 40 | 41 | def detectRStringClasses(clazz: Class[_]): Seq[Class[_]] = { 42 | findRInnerClass(clazz, "string").toSeq ++ Seq(classOf[android.R.string], classOf[com.github.scala.android.crud.res.R.string]) 43 | } 44 | 45 | def findResourceFieldWithIntValue(classes: Seq[Class[_]], value: Int): Option[Field] = 46 | findMatchingResourceField(classes, field => field.getInt(null) == value) 47 | 48 | def resourceFieldWithIntValue(classes: Seq[Class[_]], value: Int): Field = 49 | findResourceFieldWithIntValue(classes, value).getOrElse { 50 | classes.foreach(rStringClass => logError("Contents of " + rStringClass + " are " + rStringClass.getFields.mkString(", "))) 51 | throw new IllegalStateException("Unable to find R.id with value " + value + " not found. You may want to run the CrudUIGenerator.generateLayouts." + 52 | classes.mkString("(string classes: ", ",", ")")) 53 | } 54 | 55 | def findResourceFieldWithName(classes: Seq[Class[_]], name: String): Option[Field] = 56 | findMatchingResourceField(classes, field => field.getName == name) 57 | 58 | def findResourceIdWithName(classes: Seq[Class[_]], name: String): Option[Int] = 59 | findResourceFieldWithName(classes, name).map(_.getInt(null)) 60 | 61 | def resourceIdWithName(classes: Seq[Class[_]], name: String): Int = 62 | findResourceIdWithName(classes, name).getOrElse { 63 | classes.foreach(rStringClass => logError("Contents of " + rStringClass + " are " + rStringClass.getFields.mkString(", "))) 64 | throw new IllegalStateException("R.string." + name + " not found. You may want to run the CrudUIGenerator.generateLayouts." + 65 | classes.mkString("(string classes: ", ",", ")")) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/CapturedImageView.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import android.net.Uri 4 | import android.widget.ImageView 5 | import com.github.scala.android.crud.res.R 6 | import android.content.Intent 7 | import java.io.File 8 | import android.os.Environment 9 | import android.provider.MediaStore 10 | import com.github.triangle._ 11 | import com.github.scala.android.crud.common.CachedFunction 12 | import com.github.scala.android.crud.common.Common.withCloseable 13 | import android.graphics.BitmapFactory 14 | import android.graphics.drawable.{BitmapDrawable, Drawable} 15 | import com.github.scala.android.crud.action._ 16 | import com.github.scala.android.crud.CrudContextField 17 | 18 | /** A ViewField for an image that can be captured using the camera. 19 | * It currently puts the image into external storage, which requires the following in the AndroidManifest.xml: 20 | * {{{}}} 21 | * @author Eric Pabst (epabst@gmail.com) 22 | */ 23 | object CapturedImageView extends ViewField[Uri](new FieldLayout { 24 | def displayXml = 25 | def editXml = 26 | }) { 27 | private object DrawableByUriCache extends ContextVar[CachedFunction[Uri,Drawable]] 28 | 29 | private def bitmapFactoryOptions = { 30 | val options = new BitmapFactory.Options 31 | options.inDither = true 32 | //todo make this depend on the actual image's dimensions 33 | options.inSampleSize = 4 34 | options 35 | } 36 | 37 | private def setImageUri(imageView: ImageView, uriOpt: Option[Uri], contextVars: ContextVars) { 38 | imageView.setImageBitmap(null) 39 | uriOpt match { 40 | case Some(uri) => 41 | imageView.setTag(uri.toString) 42 | val contentResolver = imageView.getContext.getContentResolver 43 | val cachingResolver = DrawableByUriCache.getOrSet(contextVars, CachedFunction(uri => { 44 | withCloseable(contentResolver.openInputStream(uri)) { stream => 45 | new BitmapDrawable(BitmapFactory.decodeStream(stream, null, bitmapFactoryOptions)) 46 | } 47 | })) 48 | imageView.setImageDrawable(cachingResolver(uri)) 49 | case None => 50 | imageView.setImageResource(R.drawable.android_camera_256) 51 | } 52 | } 53 | 54 | private def tagToUri(tag: Object): Option[Uri] = Option(tag.asInstanceOf[String]).map(Uri.parse(_)) 55 | 56 | private def imageUri(imageView: ImageView): Option[Uri] = tagToUri(imageView.getTag) 57 | 58 | // This could be any value. Android requires that it is some entry in R. 59 | val DefaultValueTagKey = R.drawable.icon 60 | 61 | lazy val dcimDirectory: File = { 62 | val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) 63 | dir.mkdirs() 64 | dir 65 | } 66 | 67 | protected val delegate = GetterFromItem { 68 | case OperationResponseExtractor(Some(response)) && ViewExtractor(Some(view)) => 69 | Option(response.intent).map(_.getData).orElse(tagToUri(view.getTag(DefaultValueTagKey))) 70 | } + Getter((v: ImageView) => imageUri(v)) + SetterUsingItems[Uri] { 71 | case (ViewExtractor(Some(view: ImageView)), CrudContextField(Some(crudContext))) => uri => 72 | setImageUri(view, uri, crudContext.vars) 73 | } + OnClickOperationSetter(view => StartActivityForResultOperation(view, { 74 | val intent = new Intent("android.media.action.IMAGE_CAPTURE") 75 | val imageUri = Uri.fromFile(File.createTempFile("image", ".jpg", dcimDirectory)) 76 | intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri) 77 | view.setTag(DefaultValueTagKey, imageUri.toString) 78 | intent 79 | })) 80 | } 81 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/EntityAdapter.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import android.widget.BaseAdapter 4 | import com.github.scala.android.crud.common.PlatformTypes._ 5 | import scala.Predef._ 6 | import com.github.scala.android.crud.persistence.EntityType 7 | import android.view.{LayoutInflater, ViewGroup, View} 8 | 9 | /** An Android Adapter for an EntityType with the result of EntityPersistence.findAll. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | class EntityAdapter(val entityType: EntityType, values: Seq[AnyRef], rowLayout: ViewKey, 13 | contextItems: List[AnyRef], layoutInflater: LayoutInflater) extends BaseAdapter with AdapterCaching { 14 | def getCount: Int = values.size 15 | 16 | def getItemId(position: Int): ID = getItem(position) match { 17 | case entityType.IdField(Some(id)) => id 18 | case _ => position 19 | } 20 | 21 | def getItem(position: Int) = values(position) 22 | 23 | def getView(position: Int, convertView: View, parent: ViewGroup): View = { 24 | val view = if (convertView == null) layoutInflater.inflate(rowLayout, parent, false) else convertView 25 | bindViewFromCacheOrItems(view, getItem(position), contextItems, position, parent) 26 | view 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/EntityView.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.github.scala.android.crud.persistence.EntityType 4 | import com.github.scala.android.crud.common.PlatformTypes.ID 5 | import com.github.triangle.PortableField._ 6 | import com.github.triangle.{SetterUsingItems, Getter} 7 | import com.github.triangle.&& 8 | import android.widget._ 9 | import android.view.View 10 | import android.app.Activity 11 | import xml.NodeSeq 12 | import com.github.scala.android.crud.{CrudContextField, UriField, BaseCrudActivity, CrudContext} 13 | 14 | /** A ViewField that allows choosing a specific entity of a given EntityType or displaying its fields' values. 15 | * The layout for the EntityType that contains this EntityView may refer to fields of this view's EntityType 16 | * in the same way as referring to its own fields. If both have a field of the same name, the behavior is undefined. 17 | * @author Eric Pabst (epabst@gmail.com) 18 | */ 19 | case class EntityView(entityType: EntityType) 20 | extends ViewField[ID](FieldLayout(displayXml = NodeSeq.Empty, editXml = )) { 21 | 22 | protected val itemViewResourceId = _root_.android.R.layout.simple_spinner_dropdown_item 23 | 24 | private object AndroidUIElement { 25 | def unapply(target: AnyRef): Option[AnyRef] = target match { 26 | case view: View => Some(view) 27 | case activity: Activity => Some(activity) 28 | case _ => None 29 | } 30 | } 31 | 32 | protected val delegate = Getter[AdapterView[BaseAdapter], ID](v => Option(v.getSelectedItemId)) + SetterUsingItems[ID] { 33 | case (adapterView: AdapterView[BaseAdapter], UriField(Some(uri)) && CrudContextField(Some(crudContext @ CrudContext(crudActivity: BaseCrudActivity, _)))) => idOpt: Option[ID] => 34 | if (idOpt.isDefined || adapterView.getAdapter == null) { 35 | val crudType = crudContext.application.crudType(entityType) 36 | //don't do it again if already done from a previous time 37 | if (adapterView.getAdapter == null) { 38 | crudType.setListAdapter(adapterView, entityType, uri, crudContext, crudActivity.contextItems, crudActivity, itemViewResourceId) 39 | } 40 | if (idOpt.isDefined) { 41 | val adapter = adapterView.getAdapter 42 | val position = (0 to (adapter.getCount - 1)).view.map(adapter.getItemId(_)).indexOf(idOpt.get) 43 | adapterView.setSelection(position) 44 | } 45 | } 46 | case (AndroidUIElement(uiElement), items @ UriField(Some(baseUri)) && CrudContextField(Some(crudContext))) => idOpt: Option[ID] => 47 | val entityOpt = idOpt.flatMap(id => crudContext.withEntityPersistence(entityType)(_.find(id, baseUri))) 48 | entityType.copyFromItem(entityOpt.getOrElse(UseDefaults) +: items, uiElement) 49 | } 50 | 51 | override def toString = "EntityView(" + entityType.entityName + ")" 52 | } 53 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/EnumerationView.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.github.triangle.PortableField._ 4 | import com.github.triangle.ValueFormat._ 5 | import android.widget.{AdapterView, ArrayAdapter, BaseAdapter} 6 | import com.github.triangle.Getter 7 | import scala.collection.JavaConversions._ 8 | 9 | /** A ViewField for an [[scala.Enumeration]]. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | case class EnumerationView[E <: Enumeration#Value](enum: Enumeration) 13 | extends ViewField[E](FieldLayout(displayXml = , editXml = )) { 14 | 15 | private val itemViewResourceId = _root_.android.R.layout.simple_spinner_dropdown_item 16 | private val valueArray: List[E] = enum.values.toList.asInstanceOf[List[E]] 17 | 18 | protected val delegate = Getter[AdapterView[BaseAdapter],E](v => Option(v.getSelectedItem.asInstanceOf[E])). 19 | withSetter { adapterView => valueOpt => 20 | //don't do it again if already done from a previous time 21 | if (adapterView.getAdapter == null) { 22 | val adapter = new ArrayAdapter[E](adapterView.getContext, itemViewResourceId, valueArray) 23 | adapterView.setAdapter(adapter) 24 | } 25 | adapterView.setSelection(valueOpt.map(valueArray.indexOf(_)).getOrElse(AdapterView.INVALID_POSITION)) 26 | } + formatted[E](enumFormat(enum), ViewField.textView) 27 | 28 | override def toString = "EnumerationView(" + enum.getClass.getSimpleName + ")" 29 | } 30 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/FieldLayout.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import xml.NodeSeq 4 | 5 | 6 | /** The layout piece for a field. 7 | * It provides the XML for the part of an Android Layout that corresponds to a single field. 8 | * Standards attributes are separately added such as android:id and those needed by the parent View. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | abstract class FieldLayout { self => 12 | def displayXml: NodeSeq 13 | def editXml: NodeSeq 14 | /** Returns a similar FieldLayout but where the editXml is overridden to be empty. */ 15 | lazy val suppressEdit: FieldLayout = new FieldLayout { 16 | def displayXml = self.displayXml 17 | def editXml = NodeSeq.Empty 18 | } 19 | /** Returns a similar FieldLayout but where the displayXml is overridden to be empty. */ 20 | lazy val suppressDisplay: FieldLayout = new FieldLayout { 21 | def displayXml = NodeSeq.Empty 22 | def editXml = self.editXml 23 | } 24 | } 25 | 26 | object FieldLayout { 27 | def apply(displayXml: NodeSeq, editXml: NodeSeq): FieldLayout = { 28 | val _displayXml = displayXml 29 | val _editXml = editXml 30 | new FieldLayout { 31 | def displayXml = _displayXml 32 | def editXml = _editXml 33 | } 34 | } 35 | 36 | def textLayout(inputType: String) = new FieldLayout { 37 | def displayXml = 38 | def editXml = 39 | } 40 | 41 | lazy val noLayout = FieldLayout(NodeSeq.Empty, NodeSeq.Empty) 42 | lazy val nameLayout = textLayout("textCapWords") 43 | lazy val intLayout = textLayout("number|numberSigned") 44 | lazy val longLayout = textLayout("number|numberSigned") 45 | lazy val doubleLayout = textLayout("numberDecimal|numberSigned") 46 | lazy val currencyLayout = textLayout("numberDecimal|numberSigned") 47 | lazy val datePickerLayout = new FieldLayout { 48 | def displayXml = 49 | def editXml = 50 | } 51 | lazy val dateTextLayout = textLayout("date") 52 | 53 | private[crud] def toDisplayName(id: String): String = { 54 | var makeUpperCase = true 55 | val displayName = id.collect { 56 | case c if Character.isUpperCase(c) => 57 | makeUpperCase = false 58 | " " + c 59 | case '_' => 60 | makeUpperCase = true 61 | " " 62 | case c if makeUpperCase => 63 | makeUpperCase = false 64 | Character.toUpperCase(c) 65 | case c => c.toString 66 | }.mkString 67 | displayName.stripPrefix(" ") 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/OnClickOperationSetter.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import android.view.View 4 | import com.github.scala.android.crud.action.{ActivityWithVars, Operation} 5 | import com.github.scala.android.crud.common.UriPath 6 | import com.github.triangle.SetterUsingItems 7 | import com.github.scala.android.crud.view.AndroidConversions._ 8 | import com.github.scala.android.crud.{CrudContextField, CrudContext} 9 | 10 | /** A Setter that invokes an Operation when the View is clicked. 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | case class OnClickOperationSetter[T](viewOperation: View => Operation) extends SetterUsingItems[T] { 14 | override def setterUsingItems = { 15 | case (view: View, CrudContextField(Some(CrudContext(activity: ActivityWithVars, _)))) => ignoredValue => { 16 | if (view.isClickable) { 17 | view.setOnClickListener { view: View => 18 | viewOperation(view).invoke(UriPath.EMPTY, activity) 19 | } 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/ViewField.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.github.scala.android.crud.common.PlatformTypes._ 4 | import com.github.triangle._ 5 | import PortableField._ 6 | import java.util.{Calendar, Date, GregorianCalendar} 7 | import FieldLayout._ 8 | import com.github.triangle.Converter._ 9 | import android.widget._ 10 | import com.github.scala.android.crud.view.AndroidResourceAnalyzer._ 11 | import android.view.View 12 | 13 | /** A Map of ViewKey with values. 14 | * Wraps a map so that it is distinguished from persisted fields. 15 | */ 16 | case class ViewKeyMap(map: Map[ViewKey,Any]) { 17 | def get(key: ViewKey) = map.get(key) 18 | def iterator = map.iterator 19 | def -(key: ViewKey) = ViewKeyMap(map - key) 20 | def +[B1 >: Any](kv: (ViewKey, B1)) = ViewKeyMap(map + kv) 21 | } 22 | 23 | object ViewKeyMap { 24 | def empty = ViewKeyMap() 25 | def apply(elems: (ViewKey,Any)*): ViewKeyMap = new ViewKeyMap(Map(elems: _*)) 26 | } 27 | 28 | /** An extractor to get the View from the items being copied from. */ 29 | object ViewExtractor extends Field(identityField[View]) 30 | 31 | /** PortableField for Views. 32 | * @param defaultLayout the default layout used as an example and by [[com.github.scala.android.crud.generate.CrudUIGenerator]]. 33 | * @author Eric Pabst (epabst@gmail.com) 34 | */ 35 | abstract class ViewField[T](val defaultLayout: FieldLayout) extends DelegatingPortableField[T] { self => 36 | lazy val suppressEdit: ViewField[T] = ViewField[T](defaultLayout.suppressEdit, this) 37 | lazy val suppressDisplay: ViewField[T] = ViewField[T](defaultLayout.suppressDisplay, this) 38 | 39 | def withDefaultLayout(newDefaultLayout: FieldLayout): ViewField[T] = ViewField[T](newDefaultLayout, this) 40 | } 41 | 42 | object ViewField { 43 | def viewId[T](viewRef: ViewRef, childViewField: PortableField[T]): PortableField[T] = 44 | new ViewIdField[T](viewRef, childViewField).withViewKeyMapField 45 | 46 | def viewId[T](viewResourceId: ViewKey, childViewField: PortableField[T]): PortableField[T] = 47 | viewId(ViewRef(viewResourceId), childViewField) 48 | 49 | /** This should be used when R.id doesn't yet have the needed name, and used like this: 50 | * {{{viewId(classOf[R.id], "name", ...)}}} 51 | * Which is conceptually identical to 52 | * {{{viewId(R.id.name, ...)}}}. 53 | */ 54 | def viewId[T](rIdClass: Class[_], viewResourceIdName: String, childViewField: PortableField[T]): PortableField[T] = 55 | viewId(ViewRef(viewResourceIdName, detectRIdClasses(rIdClass)), childViewField) 56 | 57 | def apply[T](defaultLayout: FieldLayout, dataField: PortableField[T]): ViewField[T] = new ViewField[T](defaultLayout) { 58 | protected def delegate = dataField 59 | } 60 | 61 | val textView: ViewField[String] = new ViewField[String](nameLayout) { 62 | val delegate = Getter[TextView,String](v => toOption(v.getText.toString.trim)).withSetter(v => v.setText(_), _.setText("")) 63 | override def toString = "textView" 64 | } 65 | def formattedTextView[T](toDisplayString: Converter[T,String], toEditString: Converter[T,String], 66 | fromString: Converter[String,T], defaultLayout: FieldLayout = nameLayout): ViewField[T] = 67 | new ViewField[T](defaultLayout) { 68 | val delegate = Getter[TextView,T](view => toOption(view.getText.toString.trim).flatMap(fromString.convert(_))) + 69 | Setter[T] { 70 | case view: EditText => value => view.setText(value.flatMap(toEditString.convert(_)).getOrElse("")) 71 | case view: TextView => value => view.setText(value.flatMap(toDisplayString.convert(_)).getOrElse("")) 72 | } 73 | 74 | override def toString = "formattedTextView" 75 | } 76 | def textViewWithInputType(inputType: String): ViewField[String] = textView.withDefaultLayout(textLayout(inputType)) 77 | lazy val phoneView: ViewField[String] = textViewWithInputType("phone") 78 | lazy val doubleView: ViewField[Double] = ViewField[Double](doubleLayout, formatted(textView)) 79 | lazy val percentageView: ViewField[Float] = formattedTextView[Float](percentageToString, percentageToEditString, stringToPercentage, doubleLayout) 80 | lazy val currencyView = formattedTextView[Double](currencyToString, currencyToEditString, stringToCurrency, currencyLayout) 81 | lazy val intView: ViewField[Int] = ViewField[Int](intLayout, formatted[Int](textView)) 82 | lazy val longView: ViewField[Long] = ViewField[Long](longLayout, formatted[Long](textView)) 83 | 84 | private def toOption(string: String): Option[String] = if (string == "") None else Some(string) 85 | 86 | private val calendarPickerField = Setter[Calendar] { 87 | case picker: DatePicker => valueOpt => 88 | val calendar = valueOpt.getOrElse(Calendar.getInstance()) 89 | picker.updateDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)) 90 | } + Getter((p: DatePicker) => Some(new GregorianCalendar(p.getYear, p.getMonth, p.getDayOfMonth))) 91 | 92 | implicit val dateView: ViewField[Date] = new ViewField[Date](datePickerLayout) { 93 | val delegate = formattedTextView(dateToDisplayString, dateToString, stringToDate) + 94 | converted(dateToCalendar, calendarPickerField, calendarToDate) 95 | override def toString = "dateView" 96 | } 97 | 98 | val calendarDateView: ViewField[Calendar] = new ViewField[Calendar](datePickerLayout) { 99 | val delegate = converted(calendarToDate, dateView, dateToCalendar) 100 | override def toString = "calendarDateView" 101 | } 102 | 103 | @deprecated("use EnumerationView") 104 | def enumerationView[E <: Enumeration#Value](enum: Enumeration): ViewField[E] = EnumerationView[E](enum) 105 | } 106 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/ViewIdField.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.github.triangle.PortableField._ 4 | import android.view.View 5 | import android.app.Activity 6 | import com.github.triangle.{Getter, PartialDelegatingField, PortableField} 7 | import com.github.scala.android.crud.action.OperationResponse 8 | 9 | /** PortableField for a View resource within a given parent View */ 10 | class ViewIdField[T](val viewRef: ViewRef, childViewField: PortableField[T]) extends PartialDelegatingField[T] { 11 | private lazy val viewKeyMapField: PortableField[T] = 12 | viewRef.viewKeyOpt.map { key => 13 | Getter[ViewKeyMap,T](_.get(key).map(_.asInstanceOf[T])).withTransformer(map => value => map + (key -> value), _ - key) 14 | }.getOrElse(emptyField) 15 | 16 | def withViewKeyMapField: PortableField[T] = this + viewKeyMapField 17 | 18 | object ChildView { 19 | def unapply(target: Any): Option[View] = target match { 20 | case view: View => 21 | // uses the "Alternative to the ViewHolder" pattern: http://www.screaming-penguin.com/node/7767#comment-16978 22 | viewRef.viewKeyOpt.flatMap(id => Option(view.getTag(id).asInstanceOf[View]).orElse { 23 | val foundView = Option(view.findViewById(id)) 24 | foundView.foreach(view.setTag(id, _)) 25 | foundView 26 | }) 27 | case activity: Activity => viewRef.viewKeyOpt.flatMap(id => Option(activity.findViewById(id))) 28 | case _ => None 29 | } 30 | } 31 | 32 | protected def delegate = childViewField 33 | 34 | private lazy val GivenViewId = viewRef.viewKeyOpt.getOrElse(View.NO_ID) 35 | 36 | protected def subjectGetter = { 37 | case ChildView(childView) => 38 | childView 39 | case actionResponse @ OperationResponse(GivenViewId, _) => 40 | actionResponse 41 | } 42 | 43 | override def toString = "viewId(" + viewRef + ", " + childViewField + ")" 44 | } 45 | -------------------------------------------------------------------------------- /scala-android-crud/src/main/scala/com/github/scala/android/crud/view/ViewRef.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.github.scala.android.crud.common.PlatformTypes._ 4 | import com.github.scala.android.crud.view.AndroidResourceAnalyzer._ 5 | 6 | /** 7 | * A reference to an element in the UI. It wraps a [[com.github.scala.android.crud.common.PlatformTypes.ViewKey]]. 8 | * @author Eric Pabst (epabst@gmail.com) 9 | * Date: 3/26/12 10 | * Time: 6:39 AM 11 | */ 12 | 13 | trait ViewRef { 14 | def viewKeyOpt: Option[ViewKey] 15 | 16 | def viewKeyOrError: ViewKey 17 | 18 | /** 19 | * The name of the field as a String. 20 | * @param rIdClasses a list of R.id classes that may contain the id. 21 | * @throws IllegalStateException if it cannot be determined 22 | */ 23 | def fieldName(rIdClasses: Seq[Class[_]]): String 24 | 25 | override def toString: String 26 | } 27 | 28 | object ViewRef { 29 | def apply(viewKey: ViewKey): ViewRef = { 30 | new ViewRef { 31 | def viewKeyOpt = Some(viewKey) 32 | 33 | def viewKeyOrError = viewKey 34 | 35 | def fieldName(rIdClasses: Seq[Class[_]]): String = { 36 | resourceFieldWithIntValue(rIdClasses, viewKey).getName 37 | } 38 | 39 | override def toString = viewKey.toString 40 | } 41 | } 42 | 43 | def apply(viewResourceIdName: String, rIdClasses: Seq[Class[_]]): ViewRef = { 44 | new ViewRef { 45 | def viewKeyOpt = findResourceIdWithName(rIdClasses, viewResourceIdName) 46 | 47 | def viewKeyOrError = resourceIdWithName(rIdClasses, viewResourceIdName) 48 | 49 | def fieldName(rIdClasses: Seq[Class[_]]): String = viewResourceIdName 50 | 51 | override def toString = viewResourceIdName 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/java/com/github/scala/android/crud/testres/R.java: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.testres; 2 | 3 | /** 4 | * An android resource for testing. 5 | * 6 | * @author Eric Pabst (epabst@gmail.com) 7 | * Date: 9/14/11 8 | * Time: 6:33 AM 9 | */ 10 | public final class R { 11 | public static final class id { 12 | public static final int foo = 123; 13 | public static final int bar = 234; 14 | } 15 | 16 | public static final class layout { 17 | public static final int my_map_entry = 200; 18 | public static final int my_map_header = 201; 19 | public static final int my_map_row = 202; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudActivitySpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.Operation 4 | import common.ReadyFuture 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import com.xtremelabs.robolectric.RobolectricTestRunner 8 | import persistence.CursorField 9 | import scala.collection.mutable 10 | import org.scalatest.matchers.MustMatchers 11 | import Operation._ 12 | import android.widget.ListAdapter 13 | import java.lang.IllegalStateException 14 | import common.UriPath 15 | import org.scalatest.mock.MockitoSugar 16 | import org.mockito.Mockito._ 17 | import org.mockito.Matchers._ 18 | import actors.Future 19 | 20 | /** A test for [[com.github.scala.android.crud.CrudListActivity]]. 21 | * @author Eric Pabst (epabst@gmail.com) 22 | */ 23 | @RunWith(classOf[RobolectricTestRunner]) 24 | class CrudActivitySpec extends MockitoSugar with MustMatchers { 25 | val persistenceFactory = mock[PersistenceFactory] 26 | val persistence = mock[CrudPersistence] 27 | val listAdapter = mock[ListAdapter] 28 | val application = mock[CrudApplication] 29 | 30 | @Test 31 | def shouldSupportAddingWithoutEverFinding() { 32 | stub(application.actionsForEntity(any())).toReturn(Nil) 33 | val _crudType = new MyCrudType(persistence) 34 | val entity = Map[String,Any]("name" -> "Bob", "age" -> 25) 35 | val uri = UriPath(_crudType.entityName) 36 | val activity = new CrudActivity { 37 | override lazy val crudType = _crudType 38 | override def crudApplication = application 39 | 40 | override lazy val currentAction = UpdateActionName 41 | override def currentUriPath = uri 42 | override def future[T](body: => T) = new ReadyFuture[T](body) 43 | } 44 | activity.onCreate(null) 45 | _crudType.entityType.copy(entity, activity) 46 | activity.onBackPressed() 47 | verify(persistence).save(None, Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> uri.toString)) 48 | verify(persistence, never()).find(uri) 49 | } 50 | 51 | @Test 52 | def shouldAddIfIdNotFound() { 53 | stub(application.actionsForEntity(any())).toReturn(Nil) 54 | val _crudType = new MyCrudType(persistence) 55 | val entity = mutable.Map[String,Any]("name" -> "Bob", "age" -> 25) 56 | val uri = UriPath(_crudType.entityName) 57 | val activity = new CrudActivity { 58 | override lazy val crudType = _crudType 59 | override def crudApplication = application 60 | 61 | override lazy val currentAction = UpdateActionName 62 | override def currentUriPath = uri 63 | override def future[T](body: => T) = new ReadyFuture[T](body) 64 | } 65 | when(persistence.find(uri)).thenReturn(None) 66 | activity.onCreate(null) 67 | _crudType.entityType.copy(entity, activity) 68 | activity.onBackPressed() 69 | verify(persistence).save(None, mutable.Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> uri.toString)) 70 | } 71 | 72 | @Test 73 | def shouldAllowUpdating() { 74 | stub(application.actionsForEntity(any())).toReturn(Nil) 75 | val _crudType = new MyCrudType(persistence) 76 | val entity = mutable.Map[String,Any]("name" -> "Bob", "age" -> 25) 77 | val uri = UriPath(_crudType.entityName, "101") 78 | stub(persistence.find(uri)).toReturn(Some(entity)) 79 | val activity = new CrudActivity { 80 | override lazy val crudType = _crudType 81 | override def crudApplication = application 82 | 83 | override lazy val currentAction = UpdateActionName 84 | override lazy val currentUriPath = uri 85 | override def future[T](body: => T) = new ReadyFuture[T](body) 86 | } 87 | activity.onCreate(null) 88 | val viewData = _crudType.entityType.copyAndTransform(activity, mutable.Map[String,Any]()) 89 | viewData.get("name") must be (Some("Bob")) 90 | viewData.get("age") must be (Some(25)) 91 | 92 | activity.onBackPressed() 93 | verify(persistence).save(Some(101), 94 | mutable.Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> uri.toString, CursorField.idFieldName -> 101)) 95 | } 96 | 97 | @Test 98 | def withPersistenceShouldClosePersistence() { 99 | val _crudType = new MyCrudType(persistence) 100 | val activity = new CrudActivity { 101 | override lazy val crudType = _crudType 102 | override def crudApplication = application 103 | } 104 | activity.withPersistence(p => p.findAll(UriPath.EMPTY)) 105 | verify(persistence).close() 106 | } 107 | 108 | @Test 109 | def withPersistenceShouldClosePersistenceWithFailure() { 110 | val _crudType = new MyCrudType(persistence) 111 | val activity = new CrudActivity { 112 | override lazy val crudType = _crudType 113 | override def crudApplication = application 114 | } 115 | try { 116 | activity.withPersistence(p => throw new IllegalArgumentException("intentional")) 117 | fail("should have propogated exception") 118 | } catch { 119 | case e: IllegalArgumentException => "expected" 120 | } 121 | verify(persistence).close() 122 | } 123 | 124 | @Test 125 | def shouldHandleAnyExceptionWhenSaving() { 126 | stub(application.defaultContentUri).toReturn(UriPath.EMPTY) 127 | stub(persistence.save(None, "unsaveable data")).toThrow(new IllegalStateException("intentional")) 128 | val _crudType = new MyCrudType(persistence) 129 | val activity = new CrudActivity { 130 | override lazy val crudType = _crudType 131 | override def crudApplication = application 132 | } 133 | //should not throw an exception 134 | activity.saveBasedOnUserAction(persistence, "unsaveable data") 135 | } 136 | 137 | @Test 138 | def onPauseShouldNotCreateANewIdEveryTime() { 139 | stub(application.actionsForEntity(any())).toReturn(Nil) 140 | val _crudType = new MyCrudType(persistence) 141 | val entity = mutable.Map[String,Any]("name" -> "Bob", "age" -> 25) 142 | val uri = UriPath(_crudType.entityName) 143 | when(persistence.find(uri)).thenReturn(None) 144 | stub(persistence.save(None, mutable.Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> uri.toString))).toReturn(101) 145 | val activity = new CrudActivity { 146 | override lazy val crudType = _crudType 147 | override def crudApplication = application 148 | 149 | override def future[T](body: => T): Future[T] = new ReadyFuture[T](body) 150 | } 151 | activity.setIntent(constructIntent(Operation.CreateActionName, uri, activity, null)) 152 | activity.onCreate(null) 153 | //simulate a user entering data 154 | _crudType.entityType.copy(entity, activity) 155 | activity.onBackPressed() 156 | activity.onBackPressed() 157 | verify(persistence, times(1)).save(None, mutable.Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> uri.toString)) 158 | //all but the first time should provide an id 159 | verify(persistence).save(Some(101), mutable.Map[String,Any]("name" -> "Bob", "age" -> 25, "uri" -> (uri / 101).toString, 160 | CursorField.idFieldName -> 101)) 161 | } 162 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudApplicationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | 8 | /** A behavior specification for [[com.github.scala.android.crud.CrudApplication]]. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | @RunWith(classOf[JUnitRunner]) 12 | class CrudApplicationSpec extends Spec with MustMatchers { 13 | 14 | it("must provide a valid nameId") { 15 | val application = new CrudApplication { 16 | def name = "A diFFicult name to use as an ID" 17 | def allCrudTypes = List() 18 | def dataVersion = 1 19 | } 20 | application.nameId must be ("a_difficult_name_to_use_as_an_id") 21 | } 22 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudBackupAgentSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.CalculatedIterator 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import com.xtremelabs.robolectric.RobolectricTestRunner 7 | import org.scalatest.matchers.MustMatchers 8 | import android.widget.ListAdapter 9 | import persistence.CursorField.PersistedId 10 | import persistence.EntityType 11 | import scala.collection.mutable 12 | import org.easymock.{IAnswer, EasyMock} 13 | import EasyMock._ 14 | import com.github.triangle.PortableField._ 15 | import CrudBackupAgent._ 16 | import android.os.ParcelFileDescriptor 17 | import common.UriPath 18 | import com.github.triangle.BaseField 19 | 20 | /** A test for [[com.github.scala.android.crud.CrudBackupAgent]]. 21 | * @author Eric Pabst (epabst@gmail.com) 22 | */ 23 | @RunWith(classOf[RobolectricTestRunner]) 24 | class CrudBackupAgentSpec extends MustMatchers with CrudEasyMockSugar { 25 | @Test 26 | def calculatedIteratorShouldWork() { 27 | val values = List("a", "b", "c").toIterator 28 | val iterator = new CalculatedIterator[String] { 29 | def calculateNextValue() = if (values.hasNext) Some(values.next()) else None 30 | } 31 | iterator.next() must be ("a") 32 | iterator.hasNext must be (true) 33 | iterator.hasNext must be (true) 34 | iterator.next() must be ("b") 35 | iterator.hasNext must be (true) 36 | iterator.next() must be ("c") 37 | iterator.hasNext must be (false) 38 | iterator.hasNext must be (false) 39 | } 40 | 41 | @Test 42 | def shouldMarshallAndUnmarshall() { 43 | val map = Map[String,Any]("name" -> "George", "age" -> 35) 44 | val bytes = marshall(map) 45 | val copy = unmarshall(bytes) 46 | copy must be (map) 47 | } 48 | 49 | @Test 50 | def shouldSupportBackupAndRestore() { 51 | val application = mock[CrudApplication] 52 | val applicationB = mock[CrudApplication] 53 | val listAdapter = mock[ListAdapter] 54 | val backupTarget = mock[BackupTarget] 55 | val state1 = mock[ParcelFileDescriptor] 56 | val state1b = mock[ParcelFileDescriptor] 57 | 58 | val persistence = new MyEntityPersistence 59 | persistence.save(Some(100L), mutable.Map("name" -> "Joe", "age" -> 30)) 60 | persistence.save(Some(101L), mutable.Map("name" -> "Mary", "age" -> 28)) 61 | val persistence2 = new MyEntityPersistence 62 | persistence2.save(Some(101L), mutable.Map("city" -> "Los Angeles", "state" -> "CA")) 63 | persistence2.save(Some(104L), mutable.Map("city" -> "Chicago", "state" -> "IL")) 64 | val entityType = new MyCrudType(persistence) 65 | val entityType2 = new MyCrudType(new MyEntityType { 66 | override def entityName = "OtherMap" 67 | }, persistence2) 68 | val persistenceB = new MyEntityPersistence 69 | val persistence2B = new MyEntityPersistence 70 | val entityTypeB = new MyCrudType(persistenceB) 71 | val entityType2B = new MyCrudType(persistence2B) { 72 | override def entityName = "OtherMap" 73 | } 74 | val state0 = null 75 | var restoreItems = mutable.ListBuffer[RestoreItem]() 76 | expecting { 77 | call(application.allCrudTypes).andReturn(List[CrudType](entityType, entityType2)) 78 | backupTarget.writeEntity(eql("MyMap#100"), notNull()).andAnswer(saveRestoreItem(restoreItems)) 79 | backupTarget.writeEntity(eql("MyMap#101"), notNull()).andAnswer(saveRestoreItem(restoreItems)) 80 | backupTarget.writeEntity(eql("OtherMap#101"), notNull()).andAnswer(saveRestoreItem(restoreItems)) 81 | backupTarget.writeEntity(eql("OtherMap#104"), notNull()).andAnswer(saveRestoreItem(restoreItems)) 82 | call(applicationB.allCrudTypes).andReturn(List[CrudType](entityTypeB, entityType2B)) 83 | } 84 | whenExecuting(application, applicationB, listAdapter, backupTarget, state1, state1b) { 85 | val backupAgent = new CrudBackupAgent(application) 86 | backupAgent.onCreate() 87 | backupAgent.onBackup(state0, backupTarget, state1) 88 | backupAgent.onDestroy() 89 | 90 | persistenceB.findAll(UriPath.EMPTY).size must be (0) 91 | persistence2B.findAll(UriPath.EMPTY).size must be (0) 92 | 93 | val backupAgentB = new CrudBackupAgent(applicationB) 94 | backupAgentB.onCreate() 95 | backupAgentB.onRestore(restoreItems.toIterator, 1, state1b) 96 | backupAgentB.onDestroy() 97 | 98 | val allB = persistenceB.findAll(UriPath.EMPTY) 99 | allB.size must be (2) 100 | allB.map(PersistedId(_)) must be (List(100L, 101L)) 101 | 102 | val all2B = persistence2B.findAll(UriPath.EMPTY) 103 | all2B.size must be (2) 104 | all2B.map(PersistedId(_)) must be (List(101L, 104L)) 105 | } 106 | } 107 | 108 | def saveRestoreItem(restoreItems: mutable.ListBuffer[RestoreItem]): IAnswer[Unit] = answer { 109 | val currentArguments = EasyMock.getCurrentArguments 110 | currentArguments(1).asInstanceOf[Option[Map[String,Any]]].foreach { map => 111 | restoreItems += RestoreItem(currentArguments(0).asInstanceOf[String], map) 112 | } 113 | } 114 | 115 | @Test 116 | def shouldSkipBackupOfGeneratedTypes() { 117 | val application = mock[CrudApplication] 118 | val listAdapter = mock[ListAdapter] 119 | val backupTarget = mock[BackupTarget] 120 | val state1 = mock[ParcelFileDescriptor] 121 | val persistenceFactory = mock[GeneratedPersistenceFactory[Map[String, Any]]] 122 | val persistence = new MyEntityPersistence 123 | val entityType = new MyCrudType(persistence) 124 | val generatedType = new CrudType(new EntityType { 125 | def entityName = "Generated" 126 | def valueFields = List[BaseField](ParentField(MyEntityType), default[Int](100)) 127 | }, persistenceFactory) with StubCrudType 128 | val state0 = null 129 | expecting { 130 | call(application.allCrudTypes).andReturn(List[CrudType](entityType, generatedType)) 131 | //shouldn't call any methods on generatedPersistence 132 | } 133 | whenExecuting(application, listAdapter, backupTarget, state1) { 134 | val backupAgent = new CrudBackupAgent(application) 135 | backupAgent.onCreate() 136 | //shouldn't fail even though one is generated 137 | backupAgent.onBackup(state0, backupTarget, state1) 138 | backupAgent.onDestroy() 139 | } 140 | } 141 | } 142 | 143 | class MyEntityPersistence extends ListBufferCrudPersistence(Map.empty[String, Any], null, null) 144 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudEasyMockSugar.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import org.scalatest.mock.EasyMockSugar 4 | import org.easymock.{IAnswer, EasyMock} 5 | 6 | /** EasyMockSugar with some additions. 7 | * @author Eric Pabst (epabst@gmail.com) 8 | */ 9 | 10 | trait CrudEasyMockSugar extends EasyMockSugar { 11 | 12 | def namedMock[T <: AnyRef](name: String)(implicit manifest: Manifest[T]): T = { 13 | EasyMock.createMock(name, manifest.erasure.asInstanceOf[Class[T]]) 14 | } 15 | 16 | class CapturingAnswer[T](result: => T) extends IAnswer[T] { 17 | var params: List[Any] = Nil 18 | 19 | def answer() = { 20 | params = EasyMock.getCurrentArguments.toList 21 | result 22 | } 23 | } 24 | 25 | def capturingAnswer[T](result: => T): CapturingAnswer[T] = new CapturingAnswer({ result }) 26 | 27 | def answer[T](result: => T) = new IAnswer[T] { 28 | def answer = result 29 | } 30 | 31 | def eql[T](value: T): T = org.easymock.EasyMock.eq(value) 32 | } 33 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudMockitoSugar.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import org.scalatest.mock.MockitoSugar 4 | import org.mockito.stubbing.Answer 5 | import org.mockito.invocation.InvocationOnMock 6 | import org.mockito.{Matchers, Mockito} 7 | 8 | /** MockitoSugar with some additions. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | 12 | trait CrudMockitoSugar extends MockitoSugar { 13 | 14 | def namedMock[T <: AnyRef](name: String)(implicit manifest: Manifest[T]): T = { 15 | Mockito.mock(manifest.erasure.asInstanceOf[Class[T]], name) 16 | } 17 | 18 | class CapturingAnswer[T](result: => T) extends Answer[T] { 19 | var params: List[Any] = Nil 20 | 21 | def answer(invocation: InvocationOnMock) = { 22 | params = invocation.getArguments.toList 23 | result 24 | } 25 | } 26 | 27 | def capturingAnswer[T](result: => T): CapturingAnswer[T] = new CapturingAnswer({ result }) 28 | 29 | def answer[T](result: => T) = new Answer[T] { 30 | def answer(invocation: InvocationOnMock) = result 31 | } 32 | 33 | def answerWithInvocation[T](result: InvocationOnMock => T) = new Answer[T] { 34 | def answer(invocation: InvocationOnMock) = result(invocation) 35 | } 36 | 37 | def eql[T](value: T): T = Matchers.eq(value) 38 | } 39 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudPersistenceSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.PlatformTypes._ 4 | import org.junit.runner.RunWith 5 | import org.scalatest.junit.JUnitRunner 6 | import org.scalatest.matchers.MustMatchers 7 | import org.scalatest.Spec 8 | import com.github.scala.android.crud.common.UriPath 9 | import persistence.{ReadOnlyPersistence, MutableIdPk} 10 | 11 | /** A behavior specification for [[com.github.scala.android.crud.persistence.EntityPersistence]]. 12 | * @author Eric Pabst (epabst@gmail.com) 13 | */ 14 | @RunWith(classOf[JUnitRunner]) 15 | class CrudPersistenceSpec extends Spec with MustMatchers { 16 | class MyEntity(givenId: Option[ID] = None) extends MutableIdPk { 17 | this.id = givenId 18 | } 19 | val persistence = new SeqCrudPersistence[MyEntity] with ReadOnlyPersistence { 20 | def entityType = MyEntityType 21 | def crudContext = null 22 | def findAll(uri: UriPath) = Seq(new MyEntity(entityType.UriPathId.getter(uri))) 23 | } 24 | 25 | it("find must set IdPk.id") { 26 | val uri = persistence.entityType.toUri(100L) 27 | val Some(result) = persistence.find(uri, new MyEntity) 28 | result.id must be (Some(100L)) 29 | } 30 | 31 | it("findAll must set IdPk.id") { 32 | val uri = persistence.entityType.toUri(100L) 33 | val result = persistence.findAll(uri, new MyEntity).head 34 | result.id must be (Some(100L)) 35 | } 36 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/CrudTypeActionsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import _root_.android.content.Intent 4 | import action.{StartActivityOperation, Action} 5 | import common.UriPath 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import com.xtremelabs.robolectric.RobolectricTestRunner 9 | import org.scalatest.matchers.MustMatchers 10 | import com.github.scala.android.crud.action.Operation.toRichItent 11 | 12 | /** A test for [[com.github.scala.android.crud.CrudListActivity]]. 13 | * @author Eric Pabst (epabst@gmail.com) 14 | */ 15 | @RunWith(classOf[RobolectricTestRunner]) 16 | class CrudTypeActionsSpec extends MustMatchers with CrudMockitoSugar { 17 | //todo determine if shadowing, and run tests on real Android device as well. 18 | val isShadowing = true 19 | 20 | import MyCrudType.entityName 21 | 22 | val Action(_, createOperation: StartActivityOperation) = MyCrudType.createAction.get 23 | val Action(_, listOperation: StartActivityOperation) = MyCrudType.listAction 24 | val Action(_, displayOperation: StartActivityOperation) = MyCrudType.displayAction 25 | val Action(_, updateOperation: StartActivityOperation) = MyCrudType.updateAction.get 26 | 27 | @Test 28 | def createActionShouldHaveTheRightUri() { 29 | val activity = null 30 | createOperation.determineIntent(UriPath("foo"), activity).uriPath must 31 | be (UriPath("foo", entityName)) 32 | createOperation.determineIntent(UriPath("foo", entityName), activity).uriPath must 33 | be (UriPath("foo", entityName)) 34 | createOperation.determineIntent(UriPath("foo", entityName, "123"), activity).uriPath must 35 | be (UriPath("foo", entityName)) 36 | createOperation.determineIntent(UriPath("foo", entityName, "123", "bar"), activity).uriPath must 37 | be (UriPath("foo", entityName)) 38 | createOperation.determineIntent(UriPath(), activity).uriPath must 39 | be (UriPath(entityName)) 40 | } 41 | 42 | @Test 43 | def listActionShouldHaveTheRightUri() { 44 | val activity = null 45 | listOperation.determineIntent(UriPath("foo"), activity).uriPath must 46 | be (UriPath("foo", entityName)) 47 | listOperation.determineIntent(UriPath("foo", entityName), activity).uriPath must 48 | be (UriPath("foo", entityName)) 49 | listOperation.determineIntent(UriPath("foo", entityName, "123"), activity).uriPath must 50 | be (UriPath("foo", entityName)) 51 | listOperation.determineIntent(UriPath("foo", entityName, "123", "bar"), activity).uriPath must 52 | be (UriPath("foo", entityName)) 53 | listOperation.determineIntent(UriPath(), activity).uriPath must 54 | be (UriPath(entityName)) 55 | } 56 | 57 | @Test 58 | def displayActionShouldHaveTheRightUri() { 59 | val activity = null 60 | displayOperation.determineIntent(UriPath("foo", entityName, "35"), activity).uriPath must 61 | be (UriPath("foo", entityName, "35")) 62 | displayOperation.determineIntent(UriPath("foo", entityName, "34", "bar"), activity).uriPath must 63 | be (UriPath("foo", entityName, "34")) 64 | displayOperation.determineIntent(UriPath("foo", entityName, "34", "bar", "123"), activity).uriPath must 65 | be (UriPath("foo", entityName, "34")) 66 | } 67 | 68 | @Test 69 | def updateActionShouldHaveTheRightUri() { 70 | val activity = null 71 | updateOperation.determineIntent(UriPath("foo", entityName, "35"), activity).uriPath must 72 | be (UriPath("foo", entityName, "35")) 73 | updateOperation.determineIntent(UriPath("foo", entityName, "34", "bar"), activity).uriPath must 74 | be (UriPath("foo", entityName, "34")) 75 | updateOperation.determineIntent(UriPath("foo", entityName, "34", "bar", "123"), activity).uriPath must 76 | be (UriPath("foo", entityName, "34")) 77 | } 78 | 79 | @Test 80 | def shouldHaveTheStandardActionNames() { 81 | if (!isShadowing) { 82 | val activity = null 83 | createOperation.determineIntent(UriPath("foo"), activity).getAction must be (Intent.ACTION_INSERT) 84 | listOperation.determineIntent(UriPath("foo"), activity).getAction must be (Intent.ACTION_PICK) 85 | displayOperation.determineIntent(UriPath("foo"), activity).getAction must be (Intent.ACTION_VIEW) 86 | updateOperation.determineIntent(UriPath("foo"), activity).getAction must be (Intent.ACTION_EDIT) 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/DerivedPersistenceFactorySpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.ContextVars 4 | import common.UriPath 5 | import org.junit.runner.RunWith 6 | import org.scalatest.junit.JUnitRunner 7 | import org.scalatest.Spec 8 | import org.scalatest.matchers.MustMatchers 9 | import persistence.EntityType 10 | import org.mockito.Mockito._ 11 | 12 | /** A specification for [[com.github.scala.android.crud.DerivedPersistenceFactory]]. 13 | * @author Eric Pabst (epabst@gmail.com) 14 | */ 15 | @RunWith(classOf[JUnitRunner]) 16 | class DerivedPersistenceFactorySpec extends Spec with MustMatchers with CrudMockitoSugar { 17 | it("must instantiate the CrudPersistence for the delegate CrudTypes and make them available") { 18 | val entityType1 = new MyEntityType 19 | val entityType2 = new MyEntityType 20 | val persistence1 = mock[CrudPersistence] 21 | val persistence2 = mock[CrudPersistence] 22 | val factory = new DerivedPersistenceFactory[String](entityType1, entityType2) { 23 | def findAll(entityType: EntityType, uri: UriPath, delegatePersistenceMap: Map[EntityType, CrudPersistence]) = { 24 | delegatePersistenceMap must be (Map(entityType1 -> persistence1, entityType2 -> persistence2)) 25 | List("findAll", "was", "called") 26 | } 27 | } 28 | val crudContext = mock[CrudContext] 29 | stub(crudContext.vars).toReturn(new ContextVars {}) 30 | when(crudContext.openEntityPersistence(entityType1)).thenReturn(persistence1) 31 | when(crudContext.openEntityPersistence(entityType2)).thenReturn(persistence2) 32 | val persistence = factory.createEntityPersistence(mock[EntityType], crudContext) 33 | persistence.findAll(UriPath()) must be (List("findAll", "was", "called")) 34 | } 35 | 36 | it("must close each delegate CrudPersistence when close is called") { 37 | val entityType1 = mock[EntityType] 38 | val entityType2 = mock[EntityType] 39 | val factory = new DerivedPersistenceFactory[String](entityType1, entityType2) { 40 | def findAll(entityType: EntityType, uri: UriPath, delegatePersistenceMap: Map[EntityType, CrudPersistence]) = Nil 41 | } 42 | val crudContext = mock[CrudContext] 43 | stub(crudContext.vars).toReturn(new ContextVars {}) 44 | val persistence1 = mock[CrudPersistence] 45 | val persistence2 = mock[CrudPersistence] 46 | when(crudContext.openEntityPersistence(entityType1)).thenReturn(persistence1) 47 | when(crudContext.openEntityPersistence(entityType2)).thenReturn(persistence2) 48 | val persistence = factory.createEntityPersistence(mock[EntityType], crudContext) 49 | persistence.close() 50 | verify(persistence1).close() 51 | verify(persistence2).close() 52 | } 53 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/GeneratedCrudTypeSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.ContextVars 4 | import common.UriPath 5 | import org.junit.runner.RunWith 6 | import persistence.EntityType 7 | import org.scalatest.matchers.MustMatchers 8 | import com.xtremelabs.robolectric.RobolectricTestRunner 9 | import org.junit.Test 10 | import org.mockito.Mockito._ 11 | import org.mockito.Matchers._ 12 | import com.github.triangle.PortableField._ 13 | import common.PlatformTypes._ 14 | import android.app.Activity 15 | import android.view.LayoutInflater 16 | import android.widget.{BaseAdapter, AdapterView, ListAdapter} 17 | 18 | /** A behavior specification for [[com.github.scala.android.crud.GeneratedPersistenceFactory]]. 19 | * @author Eric Pabst (epabst@gmail.com) 20 | */ 21 | 22 | @RunWith(classOf[RobolectricTestRunner]) 23 | class GeneratedCrudTypeSpec extends MustMatchers with CrudMockitoSugar { 24 | val seqPersistence = mock[SeqCrudPersistence[Map[String,Any]]] 25 | val adapterView = mock[AdapterView[BaseAdapter]] 26 | val activity = mock[Activity] 27 | val listAdapterCapture = capturingAnswer[Unit] { Unit } 28 | val generatedEntityName = "Generated" 29 | val crudContext = mock[CrudContext] 30 | val layoutInflater = mock[LayoutInflater] 31 | 32 | @Test 33 | def itsListAdapterMustGetTheItemIdUsingTheIdField() { 34 | val factory = new GeneratedPersistenceFactory[Map[String,Any]] { 35 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext) = seqPersistence 36 | } 37 | val entityType = new EntityType { 38 | override protected def idField = mapField[ID]("longId") + super.idField 39 | def entityName = generatedEntityName 40 | def valueFields = Nil 41 | } 42 | stub(activity.getLayoutInflater).toReturn(layoutInflater) 43 | val generatedCrudType = new CrudType(entityType, factory) with StubCrudType 44 | stub(crudContext.vars).toReturn(new ContextVars {}) 45 | when(adapterView.setAdapter(anyObject())).thenAnswer(listAdapterCapture) 46 | val persistence = mock[CrudPersistence] 47 | when(crudContext.openEntityPersistence(entityType)).thenReturn(persistence) 48 | val uri = UriPath.EMPTY 49 | when(persistence.findAll(uri)).thenReturn(List(Map("longId" -> 456L))) 50 | generatedCrudType.setListAdapter(adapterView, entityType, uri, crudContext, Nil, activity, 123) 51 | verify(adapterView).setAdapter(anyObject()) 52 | val listAdapter = listAdapterCapture.params(0).asInstanceOf[ListAdapter] 53 | listAdapter.getItemId(0) must be (456L) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/MyCrudType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import org.mockito.Mockito 4 | import persistence.EntityType 5 | 6 | /** A simple CrudType for testing. 7 | * @author Eric Pabst (epabst@gmail.com) 8 | */ 9 | case class MyCrudType(override val entityType: EntityType, override val persistenceFactory: PersistenceFactory) 10 | extends CrudType(entityType, persistenceFactory) with StubCrudType { 11 | 12 | def this(entityType: EntityType, persistence: CrudPersistence = Mockito.mock(classOf[CrudPersistence])) { 13 | this(entityType, new MyPersistenceFactory(persistence)) 14 | } 15 | 16 | def this(persistenceFactory: PersistenceFactory) { 17 | this(new MyEntityType, persistenceFactory) 18 | } 19 | 20 | def this(persistence: CrudPersistence) { 21 | this(new MyEntityType, persistence) 22 | } 23 | } 24 | 25 | object MyCrudType extends MyCrudType(Mockito.mock(classOf[CrudPersistence])) 26 | 27 | class MyPersistenceFactory(persistence: CrudPersistence) extends PersistenceFactory { 28 | def canSave = true 29 | 30 | override def newWritable = Map.empty[String,Any] 31 | 32 | def createEntityPersistence(entityType: EntityType, crudContext: CrudContext) = persistence 33 | } 34 | 35 | trait StubCrudType extends CrudType { 36 | override lazy val entityNameLayoutPrefix = "test" 37 | } 38 | 39 | object MyCrudApplication { 40 | def apply(crudTypes: CrudType*): CrudApplication = new CrudApplication { 41 | def name = "test app" 42 | 43 | override def primaryEntityType = crudTypes.head.entityType 44 | 45 | def allCrudTypes = crudTypes.toList 46 | 47 | def dataVersion = 1 48 | } 49 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/MyEntityType.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import common.UriPath 4 | import com.github.triangle._ 5 | import persistence.EntityType 6 | import view.ViewField._ 7 | import persistence.CursorField._ 8 | import res.R 9 | 10 | /** An EntityType for testing. 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | 14 | class MyEntityType extends EntityType { 15 | def entityName: String = "MyMap" 16 | 17 | def valueFields = List[BaseField]( 18 | persisted[String]("name") + viewId(R.id.name, textView), 19 | persisted[Int]("age") + viewId(R.id.age, intView), 20 | //here to test a non-UI field 21 | persisted[String]("uri") + Getter[UriPath,String](u => Some(u.toString))) 22 | } 23 | 24 | object MyEntityType extends MyEntityType 25 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/ParentFieldSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import org.junit.Test 4 | import org.junit.runner.RunWith 5 | import com.xtremelabs.robolectric.RobolectricTestRunner 6 | import org.scalatest.matchers.MustMatchers 7 | import common.UriPath 8 | import org.scalatest.mock.EasyMockSugar 9 | import persistence.SQLiteCriteria 10 | import ParentField.foreignKey 11 | 12 | /** A specification for [[com.github.scala.android.crud.ParentField]]. 13 | * @author Eric Pabst (epabst@gmail.com) 14 | */ 15 | @RunWith(classOf[RobolectricTestRunner]) 16 | class ParentFieldSpec extends MustMatchers with EasyMockSugar { 17 | @Test 18 | def shouldGetCriteriaCorrectlyForForeignKey() { 19 | val foreign = foreignKey(MyEntityType) 20 | val uri = UriPath(MyCrudType.entityName, "19") 21 | //add on extra stuff to make sure it is ignored 22 | val uriWithExtraStuff = uri / "foo" / 1234 23 | val criteria = foreign.copyAndTransform(uriWithExtraStuff, new SQLiteCriteria) 24 | criteria.selection must be (List(ParentField(MyEntityType).fieldName + "=19")) 25 | } 26 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/SQLitePersistenceFactorySpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud 2 | 3 | import action.{ContextVars, ContextWithVars} 4 | import android.provider.BaseColumns 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import com.xtremelabs.robolectric.RobolectricTestRunner 8 | import org.scalatest.matchers.MustMatchers 9 | import com.github.triangle._ 10 | import persistence.CursorField._ 11 | import persistence.{EntityType, CursorStream, SQLiteCriteria} 12 | import PortableField._ 13 | import scala.collection._ 14 | import mutable.Buffer 15 | import org.mockito.{Mockito, Matchers} 16 | import Mockito._ 17 | import android.app.Activity 18 | import android.database.{Cursor, DataSetObserver} 19 | import android.database.sqlite.SQLiteDatabase 20 | import android.widget.ListView 21 | 22 | /** A test for [[com.github.scala.android.crud.SQLitePersistenceFactorySpec]]. 23 | * @author Eric Pabst (epabst@gmail.com) 24 | */ 25 | @RunWith(classOf[RobolectricTestRunner]) 26 | class SQLitePersistenceFactorySpec extends MustMatchers with CrudMockitoSugar with Logging { 27 | protected def logTag = getClass.getSimpleName 28 | 29 | val runningOnRealAndroid: Boolean = try { 30 | debug("Seeing if running on Real Android...") 31 | Class.forName("com.xtremelabs.robolectric.RobolectricTestRunner") 32 | warn("NOT running on Real Android.") 33 | false 34 | } catch { 35 | case _ => { 36 | info("Running on Real Android.") 37 | true 38 | } 39 | } 40 | 41 | object TestEntityType extends EntityType { 42 | def entityName = "Test" 43 | val valueFields = List(persisted[Int]("age") + default(21)) 44 | } 45 | 46 | object TestCrudType extends CrudType(TestEntityType, SQLitePersistenceFactory) 47 | 48 | object TestApplication extends CrudApplication { 49 | val name = "Test Application" 50 | 51 | def allCrudTypes = List(TestCrudType) 52 | 53 | def dataVersion = 1 54 | } 55 | val application = TestApplication 56 | 57 | @Test 58 | def shouldUseCorrectColumnNamesForFindAll() { 59 | val crudContext = mock[CrudContext] 60 | stub(crudContext.application).toReturn(application) 61 | 62 | val persistence = new SQLiteEntityPersistence(TestEntityType, crudContext) 63 | persistence.entityTypePersistedInfo.queryFieldNames must contain(BaseColumns._ID) 64 | persistence.entityTypePersistedInfo.queryFieldNames must contain("age") 65 | } 66 | 67 | @Test 68 | def shouldCloseCursorsWhenClosing() { 69 | val crudContext = mock[CrudContext] 70 | stub(crudContext.vars).toReturn(new ContextVars {}) 71 | stub(crudContext.application).toReturn(application) 72 | 73 | val cursors = Buffer[Cursor]() 74 | val persistence = new SQLiteEntityPersistence(TestEntityType, crudContext) { 75 | override def findAll(criteria: SQLiteCriteria) = { 76 | val result = super.findAll(criteria) 77 | val CursorStream(cursor, _) = result 78 | cursors += cursor 79 | result 80 | } 81 | } 82 | val writable = TestCrudType.newWritable 83 | TestEntityType.copy(PortableField.UseDefaults, writable) 84 | val id = persistence.save(None, writable) 85 | val uri = persistence.toUri(id) 86 | persistence.find(uri) 87 | persistence.findAll(new SQLiteCriteria()) 88 | cursors.size must be (2) 89 | persistence.close() 90 | for (cursor <- cursors.toList) { 91 | cursor.isClosed must be (true) 92 | } 93 | } 94 | 95 | @Test 96 | def shouldRefreshCursorWhenDeletingAndSaving() { 97 | val activity = new CrudListActivity { 98 | override def crudApplication = application 99 | override val getListView: ListView = new ListView(this) 100 | } 101 | val observer = mock[DataSetObserver] 102 | 103 | val crudContext = new CrudContext(activity, application) 104 | TestCrudType.setListAdapterUsingUri(crudContext, activity) 105 | val listAdapter = activity.getListView.getAdapter 106 | listAdapter.getCount must be (0) 107 | 108 | val writable = TestCrudType.newWritable 109 | TestEntityType.copy(PortableField.UseDefaults, writable) 110 | val id = TestCrudType.withEntityPersistence(crudContext) { _.save(None, writable) } 111 | //it must have refreshed the listAdapter 112 | listAdapter.getCount must be (if (runningOnRealAndroid) 1 else 0) 113 | 114 | TestEntityType.copy(Map("age" -> 50), writable) 115 | listAdapter.registerDataSetObserver(observer) 116 | TestCrudType.withEntityPersistence(crudContext) { _.save(Some(id), writable) } 117 | //it must have refreshed the listAdapter (notified the observer) 118 | listAdapter.unregisterDataSetObserver(observer) 119 | listAdapter.getCount must be (if (runningOnRealAndroid) 1 else 0) 120 | 121 | TestCrudType.withEntityPersistence(crudContext) { _.delete(TestEntityType.toUri(id)) } 122 | //it must have refreshed the listAdapter 123 | listAdapter.getCount must be (0) 124 | } 125 | 126 | @Test 127 | def tableNameMustNotBeReservedWord() { 128 | tableNameMustNotBeReservedWord("Group") 129 | tableNameMustNotBeReservedWord("Table") 130 | tableNameMustNotBeReservedWord("index") 131 | } 132 | 133 | def tableNameMustNotBeReservedWord(name: String) { 134 | SQLitePersistenceFactory.toTableName(name) must be (name + "0") 135 | } 136 | 137 | @Test 138 | def onCreateShouldCreateTables() { 139 | val context = mock[MyContextWithVars] 140 | val dbSetup = new GeneratedDatabaseSetup(CrudContext(context, application)) 141 | val db = mock[SQLiteDatabase] 142 | dbSetup.onCreate(db) 143 | verify(db, times(1)).execSQL(Matchers.contains("CREATE TABLE IF NOT EXISTS")) 144 | } 145 | 146 | @Test 147 | def onUpgradeShouldCreateMissingTables() { 148 | val context = mock[MyContextWithVars] 149 | val dbSetup = new GeneratedDatabaseSetup(CrudContext(context, application)) 150 | val db = mock[SQLiteDatabase] 151 | dbSetup.onUpgrade(db, 1, 2) 152 | verify(db, times(1)).execSQL(Matchers.contains("CREATE TABLE IF NOT EXISTS")) 153 | } 154 | } 155 | 156 | class MyContextWithVars extends Activity with ContextWithVars 157 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/action/ContextVarsSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.action 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | import org.mockito.Mockito._ 8 | import org.scalatest.mock.MockitoSugar 9 | 10 | /** A behavior specification for [[com.github.scala.android.crud.action.ContextVars]]. 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | 14 | @RunWith(classOf[JUnitRunner]) 15 | class ContextVarsSpec extends Spec with MustMatchers with MockitoSugar { 16 | it("must retain its value for the same Context") { 17 | object myVar extends ContextVar[String] 18 | val context = new ContextVars {} 19 | val context2 = new ContextVars {} 20 | myVar.get(context) must be (None) 21 | myVar.set(context, "hello") 22 | myVar.get(context) must be (Some("hello")) 23 | myVar.get(context2) must be (None) 24 | myVar.get(context) must be (Some("hello")) 25 | } 26 | 27 | it("clear must clear the value for the same Context") { 28 | object myVar extends ContextVar[String] 29 | val myVar2 = new ContextVar[String] 30 | val context = new ContextVars {} 31 | val context2 = new ContextVars {} 32 | myVar.set(context, "hello") 33 | myVar2.set(context, "howdy") 34 | 35 | myVar.clear(context2) must be (None) 36 | myVar.get(context) must be (Some("hello")) 37 | 38 | myVar.clear(context) must be (Some("hello")) 39 | myVar.get(context) must be (None) 40 | 41 | myVar2.clear(context) must be (Some("howdy")) 42 | } 43 | 44 | describe("getOrSet") { 45 | object StringVar extends ContextVar[String] 46 | trait Computation { 47 | def evaluate: String 48 | } 49 | 50 | it("must evaluate and set if not set yet") { 51 | val computation = mock[Computation] 52 | when(computation.evaluate).thenReturn("result") 53 | val vars = new ContextVars {} 54 | StringVar.getOrSet(vars, computation.evaluate) must be ("result") 55 | verify(computation).evaluate 56 | } 57 | 58 | it("must evaluate only the first time") { 59 | val computation = mock[Computation] 60 | when(computation.evaluate).thenReturn("result") 61 | val vars = new ContextVars {} 62 | StringVar.getOrSet(vars, computation.evaluate) 63 | StringVar.getOrSet(vars, computation.evaluate) must be ("result") 64 | verify(computation, times(1)).evaluate 65 | } 66 | 67 | it("must not evaluate if already set") { 68 | val vars = new ContextVars {} 69 | StringVar.set(vars, "hello") 70 | StringVar.getOrSet(vars, throw new IllegalArgumentException("shouldn't happen")) must be ("hello") 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/action/OptionsMenuActivitySpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.action 2 | 3 | import org.junit.runner.RunWith 4 | import org.junit.Test 5 | import org.scalatest.matchers.MustMatchers 6 | import com.xtremelabs.robolectric.RobolectricTestRunner 7 | import android.app.Activity 8 | import org.scalatest.mock.MockitoSugar 9 | import org.mockito.Mockito._ 10 | import org.mockito.Matchers.{eq => eql, _} 11 | import android.view.{MenuItem, Menu} 12 | 13 | /** A behavior specification for [[com.github.scala.android.crud.action.OptionsMenuActivity]]. 14 | * @author Eric Pabst (epabst@gmail.com) 15 | */ 16 | @RunWith(classOf[RobolectricTestRunner]) 17 | class OptionsMenuActivitySpec extends MustMatchers with MockitoSugar { 18 | class StubOptionsMenuActivity extends Activity with OptionsMenuActivity { 19 | protected def initialOptionsMenuCommands = Nil 20 | } 21 | 22 | @Test 23 | def mustUseLatestOptionsMenuForCreate() { 24 | val activity = new StubOptionsMenuActivity 25 | activity.optionsMenuCommands = List(Command(None, Some(10))) 26 | 27 | val menu = mock[Menu] 28 | val menuItem = mock[MenuItem] 29 | stub(menu.add(anyInt(), anyInt(), anyInt(), anyInt())).toReturn(menuItem) 30 | activity.onCreateOptionsMenu(menu) 31 | verify(menu, times(1)).add(anyInt(), eql(10), anyInt(), anyInt()) 32 | } 33 | 34 | @Test 35 | def mustUseLatestOptionsMenuForPrepare_Android2() { 36 | val activity = new StubOptionsMenuActivity 37 | activity.optionsMenuCommands = List(Command(None, Some(10))) 38 | 39 | val menu = mock[Menu] 40 | activity.onPrepareOptionsMenu(menu) 41 | verify(menu, times(1)).add(anyInt(), eql(10), anyInt(), anyInt()) 42 | } 43 | 44 | @Test 45 | def mustCallInvalidateOptionsMenuAndNotRepopulateForAndroid3WhenSet() { 46 | val activity = new StubOptionsMenuActivity { 47 | var invalidated, populated = false 48 | 49 | //this will be called using reflection 50 | def invalidateOptionsMenu() { 51 | invalidated = true 52 | } 53 | 54 | override private[action] def populateMenu(menu: Menu, actions: List[Command]) { 55 | populated = true 56 | } 57 | } 58 | activity.optionsMenuCommands = List(mock[Command]) 59 | activity.invalidated must be (true) 60 | activity.populated must be (false) 61 | 62 | val menu = mock[Menu] 63 | activity.onPrepareOptionsMenu(menu) 64 | activity.populated must be (false) 65 | verify(menu, never()).clear() 66 | } 67 | 68 | @Test 69 | def mustNotRepopulateInPrepareWhenNotSet_Android3() { 70 | val activity = new StubOptionsMenuActivity { 71 | var populated = false 72 | 73 | def invalidateOptionsMenu() {} 74 | 75 | override private[action] def populateMenu(menu: Menu, actions: List[Command]) { 76 | populated = true 77 | } 78 | } 79 | val menu = mock[Menu] 80 | activity.onPrepareOptionsMenu(menu) 81 | verify(menu, never()).clear() 82 | activity.populated must be (false) 83 | } 84 | 85 | @Test 86 | def mustNotRepopulateInPrepareWhenNotSet_Android2() { 87 | val activity = new StubOptionsMenuActivity { 88 | var populated = false 89 | 90 | override private[action] def populateMenu(menu: Menu, actions: List[Command]) { 91 | populated = true 92 | } 93 | } 94 | val menu = mock[Menu] 95 | activity.onPrepareOptionsMenu(menu) 96 | verify(menu, never()).clear() 97 | activity.populated must be (false) 98 | } 99 | 100 | @Test 101 | def mustRepopulateInPrepareForAndroid2AfterSetting() { 102 | val activity = new StubOptionsMenuActivity { 103 | var populated = false 104 | 105 | override private[action] def populateMenu(menu: Menu, actions: List[Command]) { 106 | populated = true 107 | } 108 | } 109 | activity.optionsMenuCommands = List(mock[Command]) 110 | activity.populated must be (false) 111 | 112 | val menu = mock[Menu] 113 | activity.onPrepareOptionsMenu(menu) 114 | verify(menu).clear() 115 | activity.populated must be (true) 116 | } 117 | 118 | @Test 119 | def mustOnlyRepopulateOnceForAndroid2AfterSetting() { 120 | val activity = new StubOptionsMenuActivity { 121 | var populated = 0 122 | 123 | override private[action] def populateMenu(menu: Menu, actions: List[Command]) { 124 | populated += 1 125 | } 126 | } 127 | activity.optionsMenuCommands = List(mock[Command]) 128 | activity.populated must be (0) 129 | 130 | val menu = mock[Menu] 131 | activity.onPrepareOptionsMenu(menu) 132 | activity.onPrepareOptionsMenu(menu) 133 | activity.populated must be (1) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/common/CommonSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | 8 | /** A behavior specification for [[com.github.scala.android.crud.common.Common]]. 9 | * @author Eric Pabst (epabst@gmail.com) 10 | */ 11 | 12 | @RunWith(classOf[JUnitRunner]) 13 | class CommonSpec extends Spec with MustMatchers { 14 | describe("tryToEvaluate") { 15 | it("must evaluate and return the parameter") { 16 | Common.tryToEvaluate("hello" + " world") must be (Some("hello world")) 17 | } 18 | 19 | it("must return None if an exception occurs") { 20 | Common.tryToEvaluate(error("intentional")) must be (None) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/common/UriPathSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.common 2 | 3 | import org.junit.runner.RunWith 4 | import com.github.scala.android.crud.MyCrudType 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.junit.JUnitRunner 7 | import org.scalatest.Spec 8 | 9 | /** A behavior specification for [[com.github.scala.android.crud.common.UriPath]]. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class UriPathSpec extends Spec with MustMatchers { 14 | val entityName = MyCrudType.entityName 15 | 16 | describe("findId") { 17 | it("must find the id following the entity name") { 18 | UriPath("foo").findId(entityName) must be (None) 19 | UriPath(entityName).findId(entityName) must be (None) 20 | UriPath(entityName, "123").findId(entityName) must be (Some(123)) 21 | UriPath(entityName, "123", "foo").findId(entityName) must be (Some(123)) 22 | UriPath(entityName, "blah").findId(entityName) must be (None) 23 | } 24 | } 25 | 26 | describe("lastEntityNameOption") { 27 | it("must handle an empty UriPath") { 28 | UriPath.EMPTY.lastEntityNameOption must be (None) 29 | } 30 | 31 | it("must get the last entityName") { 32 | UriPath("a", "b", "c").lastEntityNameOption must be (Some("c")) 33 | } 34 | 35 | it("must get the last entityName even if followed by an ID") { 36 | UriPath("a", "1").lastEntityNameOption must be (Some("a")) 37 | UriPath("a", "1", "b", "c", "3").lastEntityNameOption must be (Some("c")) 38 | } 39 | } 40 | 41 | describe("upToIdOf") { 42 | it("must strip of whatever is after the ID") { 43 | val uri = UriPath("abc", "123", entityName, "456", "def") 44 | uri.upToIdOf(entityName) must be (UriPath("abc", "123", entityName, "456")) 45 | } 46 | 47 | it("must not fail if no ID found but also preserve what is there already") { 48 | val uri = UriPath("abc", "123", "def") 49 | uri.upToIdOf(entityName).segments.startsWith(uri.segments) must be (true) 50 | } 51 | } 52 | 53 | it("must convert from a string") { 54 | val uriPath = UriPath("abc", "123", "def") 55 | UriPath(uriPath.toString) must be (uriPath) 56 | } 57 | 58 | it("must convert from an empty uri") { 59 | UriPath(UriPath.EMPTY.toString) must be(UriPath.EMPTY) 60 | } 61 | 62 | it ("must convert from an empty string") { 63 | UriPath("") must be (UriPath.EMPTY) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/generate/CrudUIGeneratorSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.generate 2 | 3 | import org.scalatest.Spec 4 | import org.scalatest.matchers.MustMatchers 5 | import org.junit.runner.RunWith 6 | import org.scalatest.junit.JUnitRunner 7 | import com.github.scala.android.crud.persistence.CursorField._ 8 | import com.github.scala.android.crud._ 9 | import org.scalatest.mock.MockitoSugar 10 | import persistence.EntityType 11 | import testres.R 12 | import view.ViewField 13 | import ViewField._ 14 | 15 | /** A behavior specification for [[com.github.scala.android.crud.generate.CrudUIGenerator]]. 16 | * @author Eric Pabst (epabst@gmail.com) 17 | */ 18 | @RunWith(classOf[JUnitRunner]) 19 | class CrudUIGeneratorSpec extends Spec with MustMatchers with MockitoSugar { 20 | val displayName = "My Name" 21 | val viewIdFieldInfo = ViewIdFieldInfo("foo", displayName, textView) 22 | 23 | describe("fieldLayoutForHeader") { 24 | it("must show the display name") { 25 | val position = 0 26 | val fieldLayout = CrudUIGenerator.fieldLayoutForHeader(viewIdFieldInfo, position) 27 | fieldLayout.attributes.find(_.key == "text").get.value.text must be ("My Name") 28 | } 29 | 30 | it("must put the first field on the left side of the screen") { 31 | val position = 0 32 | val fieldLayout = CrudUIGenerator.fieldLayoutForHeader(viewIdFieldInfo, position) 33 | fieldLayout.attributes.find(_.key == "layout_width").get.value.text must be ("wrap_content") 34 | fieldLayout.attributes.find(_.key == "gravity").get.value.text must be ("left") 35 | } 36 | 37 | it("must put the second field on the right side of the screen") { 38 | val position = 1 39 | val fieldLayout = CrudUIGenerator.fieldLayoutForHeader(viewIdFieldInfo, position) 40 | fieldLayout.attributes.find(_.key == "layout_width").get.value.text must be ("fill_parent") 41 | fieldLayout.attributes.find(_.key == "gravity").get.value.text must be ("right") 42 | } 43 | } 44 | 45 | describe("fieldLayoutForRow") { 46 | it("must put the first field on the left side of the screen") { 47 | val position = 0 48 | val fieldLayout = CrudUIGenerator.fieldLayoutForRow(viewIdFieldInfo, position) 49 | fieldLayout.head.attributes.find(_.key == "layout_width").get.value.text must be ("wrap_content") 50 | fieldLayout.head.attributes.find(_.key == "gravity").get.value.text must be ("left") 51 | } 52 | 53 | it("must put the second field on the right side of the screen") { 54 | val position = 1 55 | val fieldLayout = CrudUIGenerator.fieldLayoutForRow(viewIdFieldInfo, position) 56 | fieldLayout.head.attributes.find(_.key == "layout_width").get.value.text must be ("fill_parent") 57 | fieldLayout.head.attributes.find(_.key == "gravity").get.value.text must be ("right") 58 | } 59 | } 60 | 61 | describe("generateValueStrings") { 62 | it("must include 'list', 'add' and 'edit' strings for modifiable entities") { 63 | val entityType = new MyEntityType { 64 | override def valueFields = List(persisted[String]("model") + viewId(classOf[R], "model", textView)) 65 | } 66 | val application = new CrudApplication { 67 | def allCrudTypes = List(new MyCrudType(entityType)) 68 | def dataVersion = 1 69 | def name = "Test App" 70 | } 71 | val valueStrings = CrudUIGenerator.generateValueStrings(EntityTypeViewInfo(entityType), application) 72 | valueStrings.foreach(println(_)) 73 | (valueStrings \\ "string").length must be (3) 74 | } 75 | 76 | it("must not include an 'add' string for unaddable entities") { 77 | val entityType = new MyEntityType { 78 | override def valueFields = List(bundleField[String]("model")) 79 | } 80 | val application = new CrudApplication { 81 | def allCrudTypes = List(new MyCrudType(entityType)) 82 | def dataVersion = 1 83 | def name = "Test App" 84 | override def isAddable(entityType: EntityType) = false 85 | } 86 | val valueStrings = CrudUIGenerator.generateValueStrings(EntityTypeViewInfo(entityType), application) 87 | valueStrings.foreach(println(_)) 88 | (valueStrings \\ "string").length must be (2) 89 | } 90 | 91 | it("must not include 'add' and 'edit' strings for unmodifiable entities") { 92 | val entityType = new MyEntityType { 93 | override def valueFields = List(bundleField[String]("model")) 94 | } 95 | val application = new CrudApplication { 96 | def allCrudTypes = List(new MyCrudType(entityType)) 97 | def dataVersion = 1 98 | def name = "Test App" 99 | override def isAddable(entityType: EntityType) = false 100 | override def isSavable(entityType: EntityType) = false 101 | } 102 | val valueStrings = CrudUIGenerator.generateValueStrings(EntityTypeViewInfo(entityType), application) 103 | valueStrings.foreach(println(_)) 104 | (valueStrings \\ "string").length must be (1) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/generate/EntityFieldInfoSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.generate 2 | 3 | import org.scalatest.Spec 4 | import org.scalatest.matchers.MustMatchers 5 | import org.junit.runner.RunWith 6 | import org.scalatest.junit.JUnitRunner 7 | import com.github.triangle.{PortableField, ValueFormat} 8 | import PortableField._ 9 | import com.github.scala.android.crud.view.ViewField._ 10 | import com.github.scala.android.crud.ParentField._ 11 | import com.github.scala.android.crud.testres.R 12 | import com.github.scala.android.crud._ 13 | import org.scalatest.mock.MockitoSugar 14 | import testres.R.id 15 | import view.EntityView 16 | 17 | /** A behavior specification for [[com.github.scala.android.crud.generate.EntityFieldInfo]]. 18 | * @author Eric Pabst (epabst@gmail.com) 19 | */ 20 | @RunWith(classOf[JUnitRunner]) 21 | class EntityFieldInfoSpec extends Spec with MustMatchers with MockitoSugar { 22 | describe("viewFields") { 23 | it("must find all ViewFields") { 24 | val dummyFormat = ValueFormat[String](s => Some(s + "."), _.stripSuffix(".")) 25 | val fieldList = mapField[String]("foo") + textView + formatted[String](dummyFormat, textView) + viewId(45, textView) 26 | val info = ViewIdFieldInfo("foo", fieldList) 27 | info.viewFields must be(List(textView, textView, textView)) 28 | } 29 | } 30 | 31 | it("must handle a viewId name that does not exist") { 32 | val fieldInfo = EntityFieldInfo(viewId(classOf[R.id], "bogus", textView), List(classOf[R])).viewIdFieldInfos.head 33 | fieldInfo.id must be ("bogus") 34 | } 35 | 36 | it("must consider a ParentField displayable if it has a viewId field") { 37 | val fieldInfo = EntityFieldInfo(ParentField(MyEntityType) + viewId(classOf[R], "foo", longView), Seq(classOf[R])) 38 | fieldInfo.isDisplayable must be (true) 39 | } 40 | 41 | it("must not include a ParentField if it has no viewId field") { 42 | val fieldInfos = EntityFieldInfo(ParentField(MyEntityType), Seq(classOf[R])).viewIdFieldInfos 43 | fieldInfos must be (Nil) 44 | } 45 | 46 | it("must not include adjustment fields") { 47 | val fieldInfos = EntityFieldInfo(adjustment[String](_ + "foo"), Seq(classOf[R])).viewIdFieldInfos 48 | fieldInfos must be (Nil) 49 | } 50 | 51 | it("must not include adjustmentInPlace fields") { 52 | val fieldInfos = EntityFieldInfo(adjustmentInPlace[StringBuffer] { s => s.append("foo"); Unit }, Seq(classOf[R])).viewIdFieldInfos 53 | fieldInfos must be (Nil) 54 | } 55 | 56 | it("must not include the default primary key field") { 57 | val fieldInfos = EntityFieldInfo(MyCrudType.entityType.IdField, Seq(classOf[R])).viewIdFieldInfos 58 | fieldInfos must be (Nil) 59 | } 60 | 61 | it("must not include a ForeignKey if it has no viewId field") { 62 | val fieldInfo = EntityFieldInfo(foreignKey(MyEntityType), Seq(classOf[R])) 63 | fieldInfo.isUpdateable must be (false) 64 | } 65 | 66 | it("must detect multiple ViewFields in the same field") { 67 | val fieldInfos = EntityFieldInfo(viewId(R.id.foo, textView) + viewId(R.id.bar, textView), Seq(classOf[R.id])).viewIdFieldInfos 68 | fieldInfos.map(_.id) must be (List("foo", "bar")) 69 | } 70 | 71 | val entityFieldInfo = EntityFieldInfo(viewId(R.id.foo, foreignKey(MyEntityType) + EntityView(MyEntityType)), Seq(classOf[id])) 72 | 73 | describe("updateableViewIdFieldInfos") { 74 | it("must not include fields whose editXml is Empty") { 75 | val info = EntityFieldInfo(viewId(R.id.foo, textView.suppressEdit), Seq(classOf[id])) 76 | val fieldInfos = info.updateableViewIdFieldInfos 77 | fieldInfos must be ('empty) 78 | } 79 | 80 | it("must provide a single field for an EntityView field to allow choosing Entity instance") { 81 | val fieldInfos = entityFieldInfo.updateableViewIdFieldInfos 82 | fieldInfos.map(_.id) must be (List("foo")) 83 | fieldInfos.map(_.layout).head.editXml.head.label must be ("Spinner") 84 | } 85 | 86 | it("must not include fields whose childView field isn't a ViewField") { 87 | val info = EntityFieldInfo(viewId(R.id.foo, mapField[String]("foo")), Seq(classOf[id])) 88 | val fieldInfos = info.updateableViewIdFieldInfos 89 | fieldInfos must be ('empty) 90 | } 91 | } 92 | 93 | describe("displayableViewIdFieldInfos") { 94 | it("must not include fields whose displayXml is Empty") { 95 | val info = EntityFieldInfo(viewId(R.id.foo, textView.suppressDisplay), Seq(classOf[id])) 96 | val fieldInfos = info.displayableViewIdFieldInfos 97 | fieldInfos must be ('empty) 98 | } 99 | 100 | it("must provide each displayable field in the referenced EntityType for an EntityView field") { 101 | val fieldInfos = entityFieldInfo.displayableViewIdFieldInfos 102 | fieldInfos must be (EntityTypeViewInfo(MyEntityType).displayableViewIdFieldInfos) 103 | } 104 | 105 | it("must not include fields whose childView field isn't a ViewField") { 106 | val info = EntityFieldInfo(viewId(R.id.foo, mapField[String]("foo")), Seq(classOf[id])) 107 | val fieldInfos = info.displayableViewIdFieldInfos 108 | fieldInfos must be ('empty) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/persistence/CursorFieldSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import android.provider.BaseColumns 4 | import org.junit.Test 5 | import org.junit.runner.RunWith 6 | import com.xtremelabs.robolectric.RobolectricTestRunner 7 | import com.github.triangle._ 8 | import PortableField._ 9 | import CursorField._ 10 | import org.scalatest.matchers.MustMatchers 11 | import org.scalatest.mock.EasyMockSugar 12 | import android.database.Cursor 13 | import com.github.scala.android.crud.common.PlatformTypes._ 14 | 15 | /** A specification for [[com.github.scala.android.crud.persistence.CursorField]]. 16 | * @author Eric Pabst (epabst@gmail.com) 17 | */ 18 | @RunWith(classOf[RobolectricTestRunner]) 19 | class CursorFieldSpec extends MustMatchers with EasyMockSugar { 20 | @Test 21 | def shouldGetColumnsForQueryCorrectly() { 22 | val foreign = persisted[ID]("foreignID") 23 | val combined = persisted[Float]("height") + default(6.0f) 24 | val fields = FieldList(CursorField.PersistedId, foreign, persisted[Int]("age"), combined) 25 | val actualFields = CursorField.queryFieldNames(fields) 26 | actualFields must be (List(BaseColumns._ID, "foreignID", "age", "height")) 27 | } 28 | 29 | @Test 30 | def persistedShouldReturnNoneIfColumnNotInCursor() { 31 | val cursor = mock[Cursor] 32 | expecting { 33 | call(cursor.getColumnIndex("name")).andReturn(-1) 34 | } 35 | whenExecuting(cursor) { 36 | val field = persisted[String]("name") 37 | field.getter(cursor) must be (None) 38 | } 39 | } 40 | 41 | @Test 42 | def shouldGetCriteriaCorrectlyForANumber() { 43 | val field = sqliteCriteria[Int]("age") + default(19) 44 | val criteria: SQLiteCriteria = field.copyAndTransform(PortableField.UseDefaults, new SQLiteCriteria) 45 | criteria.selection must be (List("age=19")) 46 | } 47 | 48 | @Test 49 | def shouldGetCriteriaCorrectlyForAString() { 50 | val field = sqliteCriteria[String]("name") + default("John Doe") 51 | val criteria: SQLiteCriteria = field.copyAndTransform(PortableField.UseDefaults, new SQLiteCriteria) 52 | criteria.selection must be (List("name=\"John Doe\"")) 53 | } 54 | 55 | @Test 56 | def shouldHandleMultipleSelectionCriteria() { 57 | val field = sqliteCriteria[Int]("age") + sqliteCriteria[Int]("alternateAge") + default(19) 58 | val criteria: SQLiteCriteria = field.copyAndTransform(PortableField.UseDefaults, new SQLiteCriteria) 59 | criteria.selection.sorted must be (List("age=19", "alternateAge=19")) 60 | } 61 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/persistence/CursorStreamSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | import org.scalatest.mock.MockitoSugar 8 | import org.mockito.Mockito._ 9 | import android.database.Cursor 10 | 11 | /** A behavior specification for [[com.github.scala.android.crud.persistence.EntityPersistence]]. 12 | * @author Eric Pabst (epabst@gmail.com) 13 | */ 14 | @RunWith(classOf[JUnitRunner]) 15 | class CursorStreamSpec extends Spec with MustMatchers with MockitoSugar { 16 | it("must handle an empty Cursor") { 17 | val field = CursorField.persisted[String]("name") 18 | val cursor = mock[Cursor] 19 | stub(cursor.moveToNext()).toReturn(false) 20 | val stream = CursorStream(cursor, EntityTypePersistedInfo(List(field))) 21 | stream.isEmpty must be (true) 22 | stream.size must be (0) 23 | stream.headOption must be (None) 24 | } 25 | 26 | it("must not instantiate the entire Stream for an infinite Cursor") { 27 | val field = CursorField.persisted[String]("name") 28 | 29 | val cursor = mock[Cursor] 30 | stub(cursor.moveToNext).toReturn(true) 31 | stub(cursor.getColumnIndex("name")).toReturn(1) 32 | stub(cursor.getString(1)).toReturn("Bryce") 33 | 34 | val stream = CursorStream(cursor, EntityTypePersistedInfo(List(field))) 35 | val second = stream.tail.head 36 | field(second) must be ("Bryce") 37 | } 38 | 39 | it("must have correct number of elements") { 40 | val field = CursorField.persisted[String]("name") 41 | 42 | val cursor = mock[Cursor] 43 | when(cursor.moveToNext).thenReturn(true).thenReturn(true).thenReturn(false) 44 | stub(cursor.getColumnIndex("name")).toReturn(1) 45 | stub(cursor.getString(1)).toReturn("Allen") 46 | 47 | val stream = CursorStream(cursor, EntityTypePersistedInfo(List(field))) 48 | stream.toList.size must be (2) 49 | } 50 | 51 | it("must have correct size") { 52 | val cursor = mock[Cursor] 53 | stub(cursor.getCount).toReturn(500) 54 | 55 | val stream = CursorStream(cursor, EntityTypePersistedInfo(List(CursorField.persisted[String]("name")))) 56 | stream.size must be (500) 57 | stream.length must be (500) 58 | } 59 | 60 | it("must allow accessing data from different positions in any order") { 61 | val field = CursorField.persisted[String]("name") 62 | 63 | val cursor = mock[Cursor] 64 | when(cursor.moveToNext).thenReturn(true).thenReturn(true).thenReturn(false) 65 | stub(cursor.getColumnIndex("name")).toReturn(1) 66 | when(cursor.getString(1)).thenReturn("Allen").thenReturn("Bryce") 67 | 68 | val stream = CursorStream(cursor, EntityTypePersistedInfo(List(field))) 69 | val second = stream.tail.head 70 | val first = stream.head 71 | field(second) must be ("Bryce") 72 | field(first) must be ("Allen") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/persistence/EntityPersistenceSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | import com.github.scala.android.crud.common.UriPath 8 | import com.github.scala.android.crud.common.PlatformTypes._ 9 | 10 | /** A behavior specification for [[com.github.scala.android.crud.persistence.EntityPersistence]]. 11 | * @author Eric Pabst (epabst@gmail.com) 12 | */ 13 | @RunWith(classOf[JUnitRunner]) 14 | class EntityPersistenceSpec extends Spec with MustMatchers { 15 | describe("find") { 16 | it("must delegate to findAll and return the first result") { 17 | val persistence = new SeqEntityPersistence[String] with ReadOnlyPersistence { 18 | def findAll(uri: UriPath) = Seq("the result") 19 | 20 | def toUri(id: ID) = throw new UnsupportedOperationException 21 | } 22 | val uri = UriPath() 23 | persistence.find(uri) must be (Some("the result")) 24 | } 25 | 26 | it("must handle no results") { 27 | val persistence = new SeqEntityPersistence[String] with ReadOnlyPersistence { 28 | def findAll(uri: UriPath) = Nil 29 | 30 | def toUri(id: ID) = throw new UnsupportedOperationException 31 | } 32 | val uri = UriPath() 33 | persistence.find(uri) must be (None) 34 | } 35 | 36 | it("must fail if multiple matches are found") { 37 | val persistence = new SeqEntityPersistence[String] with ReadOnlyPersistence { 38 | def findAll(uri: UriPath) = Seq("one", "two") 39 | 40 | def toUri(id: ID) = throw new UnsupportedOperationException 41 | } 42 | val uri = UriPath() 43 | intercept[IllegalStateException] { 44 | persistence.find(uri) 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/persistence/PersistedTypeSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.persistence 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.matchers.MustMatchers 5 | import org.scalatest.mock.EasyMockSugar 6 | import com.xtremelabs.robolectric.RobolectricTestRunner 7 | import org.junit.Test 8 | import android.os.Bundle 9 | 10 | 11 | /** A behavior specification for [[com.github.scala.android.crud.persistence.PersistedType]]. 12 | * @author Eric Pabst (epabst@gmail.com) 13 | */ 14 | 15 | @RunWith(classOf[RobolectricTestRunner]) 16 | class PersistedTypeSpec extends MustMatchers with EasyMockSugar { 17 | @Test 18 | def itMustReadAndWriteBundle() { 19 | import PersistedType._ 20 | verifyPersistedTypeWithBundle("hello") 21 | verifyPersistedTypeWithBundle(100L) 22 | } 23 | 24 | def verifyPersistedTypeWithBundle[T](value: T)(implicit persistedType: PersistedType[T]) { 25 | val bundle = new Bundle() 26 | persistedType.putValue(bundle, "foo", value) 27 | persistedType.getValue(bundle, "foo") must be (Some(value)) 28 | } 29 | 30 | @Test 31 | def itMustGiveCorrectSQLiteType() { 32 | import PersistedType._ 33 | stringType.sqliteType must be ("TEXT") 34 | blobType.sqliteType must be ("BLOB") 35 | longRefType.sqliteType must be ("INTEGER") 36 | longType.sqliteType must be ("INTEGER") 37 | intRefType.sqliteType must be ("INTEGER") 38 | intType.sqliteType must be ("INTEGER") 39 | shortRefType.sqliteType must be ("INTEGER") 40 | shortType.sqliteType must be ("INTEGER") 41 | byteRefType.sqliteType must be ("INTEGER") 42 | byteType.sqliteType must be ("INTEGER") 43 | doubleRefType.sqliteType must be ("REAL") 44 | doubleType.sqliteType must be ("REAL") 45 | floatRefType.sqliteType must be ("REAL") 46 | floatType.sqliteType must be ("REAL") 47 | } 48 | } -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/testres/SiblingToR.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.testres 2 | 3 | /** A class that is in the same package as R. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | class SiblingToR 7 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/testres/subpackage/ClassInSubpackage.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.testres.subpackage 2 | 3 | /** A class in sub-package for testing. 4 | * @author Eric Pabst (epabst@gmail.com) 5 | */ 6 | 7 | class ClassInSubpackage 8 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/validate/ValidationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.validate 2 | 3 | import org.scalatest.junit.JUnitRunner 4 | import org.junit.runner.RunWith 5 | import org.scalatest.matchers.MustMatchers 6 | import org.scalatest.Spec 7 | import Validation._ 8 | 9 | /** A behavior specification for [[com.github.scala.android.crud.validate.Validation]]. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class ValidationSpec extends Spec with MustMatchers { 14 | describe("required") { 15 | val transformer = required[Int].transformer[ValidationResult] 16 | 17 | it("must detect an empty value") { 18 | transformer(ValidationResult.Valid)(None) must be (ValidationResult(1)) 19 | } 20 | 21 | it("must accept a defined value") { 22 | transformer(ValidationResult.Valid)(Some(0)) must be (ValidationResult.Valid) 23 | } 24 | } 25 | 26 | describe("requiredAndNot") { 27 | val transformer = requiredAndNot[Int](1, 3).transformer[ValidationResult] 28 | 29 | it("must detect an empty value") { 30 | transformer(ValidationResult.Valid)(None) must be (ValidationResult(1)) 31 | } 32 | 33 | it("must detect a matching (non-empty) value and consider it invalid") { 34 | transformer(ValidationResult.Valid)(Some(3)) must be (ValidationResult(1)) 35 | } 36 | 37 | it("must detect a non-matching (non-empty) value and consider it valid") { 38 | transformer(ValidationResult.Valid)(Some(2)) must be (ValidationResult.Valid) 39 | } 40 | } 41 | 42 | describe("requiredString") { 43 | val transformer = requiredString.transformer[ValidationResult] 44 | 45 | it("must detect an empty value") { 46 | transformer(ValidationResult.Valid)(None) must be (ValidationResult(1)) 47 | } 48 | 49 | it("must consider an empty string as invalid") { 50 | transformer(ValidationResult.Valid)(Some("")) must be (ValidationResult(1)) 51 | } 52 | 53 | it("must consider an string with just whitespace as invalid") { 54 | transformer(ValidationResult.Valid)(Some(" \t\r\n ")) must be (ValidationResult(1)) 55 | } 56 | 57 | it("must consider a non-empty string as valid") { 58 | transformer(ValidationResult.Valid)(Some("hello")) must be (ValidationResult.Valid) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/view/AndroidResourceAnalyzerSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import org.scalatest.junit.JUnitRunner 4 | import org.junit.runner.RunWith 5 | import org.scalatest.Spec 6 | import org.scalatest.matchers.MustMatchers 7 | import com.github.scala.android.crud.testres._ 8 | 9 | /** A behavior specification for [[com.github.scala.android.crud.view.AndroidResourceAnalyzer]]. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | @RunWith(classOf[JUnitRunner]) 13 | class AndroidResourceAnalyzerSpec extends Spec with MustMatchers { 14 | describe("detectRIdClasses") { 15 | it("must be able to find all of the R.id instances") { 16 | AndroidResourceAnalyzer.detectRIdClasses(classOf[SiblingToR]) must 17 | be (Seq(classOf[R.id], classOf[android.R.id], classOf[com.github.scala.android.crud.res.R.id])) 18 | } 19 | 20 | it("must look in parent packages to find the application R.id instance") { 21 | AndroidResourceAnalyzer.detectRIdClasses(classOf[subpackage.ClassInSubpackage]) must 22 | be (Seq(classOf[R.id], classOf[android.R.id], classOf[com.github.scala.android.crud.res.R.id])) 23 | } 24 | } 25 | 26 | describe("detectRLayoutClasses") { 27 | it("must be able to find all of the R.layout instances") { 28 | AndroidResourceAnalyzer.detectRLayoutClasses(classOf[SiblingToR]) must 29 | be (Seq(classOf[R.layout], classOf[android.R.layout], classOf[com.github.scala.android.crud.res.R.layout])) 30 | } 31 | 32 | it("must look in parent packages to find the application R.layout instance") { 33 | AndroidResourceAnalyzer.detectRLayoutClasses(classOf[subpackage.ClassInSubpackage]) must 34 | be (Seq(classOf[R.layout], classOf[android.R.layout], classOf[com.github.scala.android.crud.res.R.layout])) 35 | } 36 | } 37 | 38 | it("must locate a resource field by name") { 39 | AndroidResourceAnalyzer.findResourceFieldWithName(List(classOf[R.id]), "foo").map(_.get(null)) must be (Some(123)) 40 | } 41 | 42 | it("must locate a resource field by value") { 43 | AndroidResourceAnalyzer.findResourceFieldWithIntValue(List(classOf[R.id]), 123).map(_.getName) must be (Some("foo")) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/view/CapturedImageViewSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.matchers.MustMatchers 5 | import ViewField._ 6 | import android.view.View 7 | import com.xtremelabs.robolectric.RobolectricTestRunner 8 | import org.junit.Test 9 | import org.scalatest.mock.MockitoSugar 10 | import org.mockito.Mockito._ 11 | import com.github.scala.android.crud.action.OperationResponse 12 | import android.net.Uri 13 | import android.content.Intent 14 | import android.widget.ImageView 15 | 16 | /** A behavior specification for [[com.github.scala.android.crud.view.ViewField]]. 17 | * @author Eric Pabst (epabst@gmail.com) 18 | */ 19 | 20 | @RunWith(classOf[RobolectricTestRunner]) 21 | class CapturedImageViewSpec extends MustMatchers with MockitoSugar { 22 | @Test 23 | def capturedImageViewMustGetImageUriFromOperationResponse() { 24 | val uri = Uri.parse("file://foo/bar.jpg") 25 | val TheViewId = 101 26 | val field = viewId(TheViewId, CapturedImageView) 27 | val outerView = mock[View] 28 | val view = mock[ImageView] 29 | val intent = mock[Intent] 30 | stub(outerView.getId).toReturn(TheViewId) 31 | stub(outerView.findViewById(TheViewId)).toReturn(view) 32 | stub(intent.getData).toReturn(uri) 33 | field.getterFromItem(List(OperationResponse(TheViewId, intent), outerView)) must be (Some(uri)) 34 | verify(view, never()).getTag(CapturedImageView.DefaultValueTagKey) 35 | } 36 | 37 | @Test 38 | def capturedImageViewMustGetImageUriFromOperationResponseEvenIfImageIsAlreadySet() { 39 | val uri = Uri.parse("file://foo/bar.jpg") 40 | val uri2 = Uri.parse("file://foo/cookie.jpg") 41 | val TheViewId = 101 42 | val field = viewId(TheViewId, CapturedImageView) 43 | val outerView = mock[View] 44 | val view = mock[ImageView] 45 | val intent = mock[Intent] 46 | stub(outerView.getId).toReturn(TheViewId) 47 | stub(outerView.findViewById(TheViewId)).toReturn(view) 48 | stub(intent.getData).toReturn(uri2) 49 | stub(view.getTag).toReturn(uri.toString) 50 | field.getterFromItem(List(OperationResponse(TheViewId, intent), outerView)) must be (Some(uri2)) 51 | } 52 | 53 | @Test 54 | def capturedImageViewMustGetImageUriFromViewTagOperationResponseDoesNotHaveIt() { 55 | val TheViewId = 101 56 | val field = viewId(TheViewId, CapturedImageView) 57 | val outerView = mock[View] 58 | val view = mock[ImageView] 59 | stub(outerView.getId).toReturn(TheViewId) 60 | stub(outerView.findViewById(TheViewId)).toReturn(view) 61 | stub(view.getTag(CapturedImageView.DefaultValueTagKey)).toReturn("file://foo/bar.jpg") 62 | field.getterFromItem(List(OperationResponse(TheViewId, null), outerView)) must be (Some(Uri.parse("file://foo/bar.jpg"))) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/view/EnumerationViewSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.matchers.MustMatchers 5 | import com.github.triangle.PortableField._ 6 | import com.xtremelabs.robolectric.RobolectricTestRunner 7 | import org.junit.Test 8 | import org.scalatest.mock.MockitoSugar 9 | import android.widget._ 10 | import java.util.Locale 11 | import android.content.Context 12 | 13 | /** A behavior specification for [[com.github.scala.android.crud.view.ViewField]]. 14 | * @author Eric Pabst (epabst@gmail.com) 15 | */ 16 | 17 | @RunWith(classOf[RobolectricTestRunner]) 18 | class EnumerationViewSpec extends MustMatchers with MockitoSugar { 19 | class MyEntity(var string: String, var number: Int) 20 | val context = mock[Context] 21 | val itemLayoutId = android.R.layout.simple_spinner_dropdown_item 22 | Locale.setDefault(Locale.US) 23 | object MyEnum extends Enumeration { 24 | val A = Value("a") 25 | val B = Value("b") 26 | val C = Value("c") 27 | } 28 | val enumerationView = EnumerationView[MyEnum.Value](MyEnum) 29 | 30 | @Test 31 | def itMustSetTheAdapterForAnAdapterView() { 32 | val adapterView = new Spinner(context) 33 | enumerationView.setValue(adapterView, Some(MyEnum.C)) 34 | val adapter = adapterView.getAdapter 35 | (0 to (adapter.getCount - 1)).toList.map(adapter.getItem(_)) must be (List(MyEnum.A, MyEnum.B, MyEnum.C)) 36 | } 37 | 38 | @Test 39 | def itMustSetTheAdapterForAnAdapterViewEvenIfTheValueIsNotSet() { 40 | val adapterView = new Spinner(context) 41 | enumerationView.setValue(adapterView, None) 42 | val adapter = adapterView.getAdapter 43 | (0 to (adapter.getCount - 1)).toList.map(adapter.getItem(_)) must be (List(MyEnum.A, MyEnum.B, MyEnum.C)) 44 | } 45 | 46 | @Test 47 | def itMustSetThePositionCorrectly() { 48 | val adapterView = new Spinner(context) 49 | enumerationView.setValue(adapterView, MyEnum.C) 50 | adapterView.getSelectedItemPosition must be (2) 51 | } 52 | 53 | @Test 54 | def itMustHandleInvalidValueForAnAdapterView() { 55 | val field = enumerationView 56 | val adapterView = new Spinner(context) 57 | field.setValue(adapterView, None) 58 | adapterView.getSelectedItemPosition must be (AdapterView.INVALID_POSITION) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/view/FieldLayoutSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import org.junit.runner.RunWith 4 | import org.scalatest.junit.JUnitRunner 5 | import org.scalatest.Spec 6 | import org.scalatest.matchers.MustMatchers 7 | import xml.NodeSeq 8 | 9 | /** A behavior specification for [[com.github.scala.android.crud.view.FieldLayout]]. 10 | * @author Eric Pabst (epabst@gmail.com) 11 | */ 12 | 13 | @RunWith(classOf[JUnitRunner]) 14 | class FieldLayoutSpec extends Spec with MustMatchers { 15 | 16 | describe("toDisplayName") { 17 | import FieldLayout._ 18 | it("must add a space before capital letters") { 19 | toDisplayName("anIdentifier").filter(_ == ' ').size must be (1) 20 | } 21 | 22 | it("must uppercase the first character") { 23 | toDisplayName("anIdentifier") must be ("An Identifier") 24 | } 25 | 26 | it("must replace '_' with a space and upper case the next letter") { 27 | toDisplayName("a_cool_identifier") must be ("A Cool Identifier") 28 | } 29 | } 30 | 31 | describe("suppressEdit") { 32 | it("must make the editXml empty but preserve the displayXml") { 33 | val fieldLayout = FieldLayout.datePickerLayout 34 | fieldLayout.suppressEdit.editXml must be (NodeSeq.Empty) 35 | fieldLayout.suppressEdit.displayXml must be (fieldLayout.displayXml) 36 | } 37 | } 38 | 39 | describe("suppressDisplay") { 40 | it("must make the displayXml empty but preserve the editXml") { 41 | val fieldLayout = FieldLayout.datePickerLayout 42 | fieldLayout.suppressDisplay.displayXml must be (NodeSeq.Empty) 43 | fieldLayout.suppressDisplay.editXml must be (fieldLayout.editXml) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scala-android-crud/src/test/scala/com/github/scala/android/crud/view/OnClickOperationSetterSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.scala.android.crud.view 2 | 3 | import com.xtremelabs.robolectric.RobolectricTestRunner 4 | import org.junit.runner.RunWith 5 | import org.junit.Test 6 | import org.scalatest.mock.MockitoSugar 7 | import org.mockito.Mockito._ 8 | import org.mockito.Matchers._ 9 | import android.view.View 10 | import com.github.scala.android.crud.action.{ActivityWithVars, Operation} 11 | import com.github.scala.android.crud.{CrudApplication, CrudContext} 12 | import com.github.triangle.PortableField 13 | import com.github.scala.android.crud.common.UriPath 14 | 15 | /** A specification of [[com.github.scala.android.crud.view.OnClickOperationSetter]]. 16 | * @author Eric Pabst (epabst@gmail.com) 17 | */ 18 | @RunWith(classOf[RobolectricTestRunner]) 19 | class OnClickOperationSetterSpec extends MockitoSugar { 20 | @Test 21 | def itMustSetOnClickListenerWhenClicableIsTrue() { 22 | val operation = mock[Operation] 23 | val view = mock[View] 24 | stub(view.isClickable).toReturn(true) 25 | val setter = OnClickOperationSetter[Unit](_ => operation) 26 | setter.setValue(view, None, List(UriPath.EMPTY, CrudContext(mock[MyActivityWithVars], mock[CrudApplication]), PortableField.UseDefaults)) 27 | verify(view).setOnClickListener(any()) 28 | } 29 | 30 | @Test 31 | def itMustNotSetOnClickListenerWhenClickableIsFalse() { 32 | val operation = mock[Operation] 33 | val view = mock[View] 34 | stub(view.isClickable).toReturn(false) 35 | val setter = OnClickOperationSetter[Unit](_ => operation) 36 | setter.setValue(view, None, List(UriPath.EMPTY, CrudContext(mock[MyActivityWithVars], mock[CrudApplication]), PortableField.UseDefaults)) 37 | verify(view, never()).setOnClickListener(any()) 38 | } 39 | } 40 | 41 | class MyActivityWithVars extends ActivityWithVars 42 | --------------------------------------------------------------------------------