├── .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 |
--------------------------------------------------------------------------------