Wiki source code of EOF-Using EOF-Custom EOAdaptor
Last modified by Pascal Robert on 2012/09/22 15:33
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 1 | It's surprisingly easy to create a simple custom EOAdaptor. You can look at JavaFSAdaptor, which is part of Project Wonder, and you can also look at JavaMemoryAdaptor, also in Wonder. | ||
| 2 | |||
| 3 | Here is a little example on how the create an EOAdaptor: | ||
| 4 | |||
| 5 | Creating an EOAdaptor can be very straightforward (or very complicated) | ||
| 6 | depending on the underlying data source. In this example, and for the | ||
| 7 | sake of keeping thing simple, I'm using the file system as a data source | ||
| 8 | (well, at least, how java.io.File sees it). | ||
| 9 | At a bare minimum you need to implement three classes: EOAdaptor, | ||
| 10 | EOAdaptorContext, EOAdaptorChannel. | ||
| 11 | |||
| 12 | ~[[[EOAdaptor JavaDoc>>url:http://wocommunity.org/documents/javadoc/WebObjects/5.4.2/com/webobjects/eoaccess/EOAdaptor.html?is-external=true||shape="rect"]]] | ||
| 13 | |||
| 14 | Not much in here. The EOAdaptor is mostly a rendezvous point to access | ||
| 15 | other functionality in your adaptor. You need to implement a couple of | ||
| 16 | abstract methods though: | ||
| 17 | |||
| 18 | (% class="alternate" %) | ||
| 19 | * assertConnectionDictionaryIsValid() to check the validity of a | ||
| 20 | connection dictionary. | ||
| 21 | * createAdaptorContext() to create your own EOAdaptorContext instance. | ||
| 22 | * defaultExpressionClass() to return a relevant EOSQLExpression class. | ||
| 23 | * isValidQualifierType() to decide if a qualifier can be used in a SQL | ||
| 24 | where clause. | ||
| 25 | * synchronizationFactory() to return a EOSchemaGeneration helper. | ||
| 26 | |||
| 27 | In the case of a file system there is not much to do: | ||
| 28 | |||
| 29 | {{code}} | ||
| 30 | |||
| 31 | public void assertConnectionDictionaryIsValid() | ||
| 32 | { | ||
| 33 | } | ||
| 34 | |||
| 35 | public EOAdaptorContext createAdaptorContext() | ||
| 36 | { | ||
| 37 | return new FSAdaptorContext( this ); | ||
| 38 | } | ||
| 39 | |||
| 40 | public Class defaultExpressionClass() | ||
| 41 | { | ||
| 42 | throw new UnsupportedOperationException | ||
| 43 | ( "FSAdaptor.defaultExpressionClass" ); | ||
| 44 | } | ||
| 45 | |||
| 46 | public EOSQLExpressionFactory expressionFactory() | ||
| 47 | { | ||
| 48 | throw new UnsupportedOperationException | ||
| 49 | ( "FSAdaptor.expressionFactory" ); | ||
| 50 | } | ||
| 51 | |||
| 52 | public boolean isValidQualifierType(String aTypeName, EOModel aModel) | ||
| 53 | { | ||
| 54 | return true; | ||
| 55 | } | ||
| 56 | |||
| 57 | public EOSchemaGeneration synchronizationFactory() | ||
| 58 | { | ||
| 59 | throw new UnsupportedOperationException | ||
| 60 | ( "FSAdaptor.synchronizationFactory" ); | ||
| 61 | } | ||
| 62 | |||
| 63 | {{/code}} | ||
| 64 | |||
| 65 | The only important method is createAdaptorContext(). | ||
| 66 | |||
| 67 | ~[[[EOAdaptorContext JavaDoc>>url:http://wocommunity.org/documents/javadoc/WebObjects/5.4.2/com/webobjects/eoaccess/EOAdaptorContext.html?is-external=true||shape="rect"]]] | ||
| 68 | |||
| 69 | The adaptor context handles "transactions". It's usually pretty simple | ||
| 70 | if your data store support that concept. Not much to do here for a file | ||
| 71 | system: | ||
| 72 | |||
| 73 | {{code}} | ||
| 74 | |||
| 75 | public void beginTransaction() | ||
| 76 | { | ||
| 77 | if ( _hasTransaction == false ) | ||
| 78 | { | ||
| 79 | _hasTransaction = true; | ||
| 80 | |||
| 81 | this.transactionDidBegin(); | ||
| 82 | } | ||
| 83 | } | ||
| 84 | |||
| 85 | public void commitTransaction() | ||
| 86 | { | ||
| 87 | if ( _hasTransaction == true ) | ||
| 88 | { | ||
| 89 | _hasTransaction = false; | ||
| 90 | |||
| 91 | this.transactionDidCommit(); | ||
| 92 | } | ||
| 93 | } | ||
| 94 | |||
| 95 | public EOAdaptorChannel createAdaptorChannel() | ||
| 96 | { | ||
| 97 | return new FSAdaptorChannel( this ); | ||
| 98 | } | ||
| 99 | |||
| 100 | public void handleDroppedConnection() | ||
| 101 | { | ||
| 102 | } | ||
| 103 | |||
| 104 | public void rollbackTransaction() | ||
| 105 | { | ||
| 106 | throw new UnsupportedOperationException | ||
| 107 | ( "FSAdaptorContext.rollbackTransaction" ); | ||
| 108 | } | ||
| 109 | |||
| 110 | {{/code}} | ||
| 111 | |||
| 112 | Again createAdaptorChannel() is the only method that you really care | ||
| 113 | about in this case. | ||
| 114 | |||
| 115 | ~[[[EOAdaptorChannel JavaDoc>>url:http://wocommunity.org/documents/javadoc/WebObjects/5.4.2/com/webobjects/eoaccess/EOAdaptorChannel.html?is-external=true||shape="rect"]]] | ||
| 116 | |||
| 117 | That's the real thing. It's where most of the work is done. It's | ||
| 118 | important to get this one right. Depending on your back end, it's simply | ||
| 119 | an exercise in translating EOQualifiers into something that your data | ||
| 120 | storage will understand. Here a the most important methods: | ||
| 121 | |||
| 122 | (% class="alternate" %) | ||
| 123 | * deleteRowsDescribedByQualifier() to handle deletion of "row" or | ||
| 124 | something equivalent. | ||
| 125 | * insertRow() to create a "row". | ||
| 126 | * updateValuesInRowsDescribedByQualifier() to update a "row". | ||
| 127 | |||
| 128 | Finally, fetching "rows" is a two steps process: first you need to setup | ||
| 129 | the "context" of what you are going to fetch and then fetch it: | ||
| 130 | |||
| 131 | (% class="alternate" %) | ||
| 132 | * selectAttributes() is where you set your fetch "context". | ||
| 133 | * fetchRow is the actual fetching one row at the time. | ||
| 134 | |||
| 135 | In the case of a file system here is what you could have: | ||
| 136 | |||
| 137 | (% class="alternate" %) | ||
| 138 | * deleteRowsDescribedByQualifier will delete some files (aka "rows" | ||
| 139 | based) on a qualifier. | ||
| 140 | |||
| 141 | {{code}} | ||
| 142 | |||
| 143 | public int deleteRowsDescribedByQualifier(EOQualifier aQualifier, | ||
| 144 | EOEntity anEntity) | ||
| 145 | { | ||
| 146 | if ( aQualifier != null ) | ||
| 147 | { | ||
| 148 | if ( anEntity != null ) | ||
| 149 | { | ||
| 150 | NSArray someFiles = | ||
| 151 | FSQualifierHandler.filesWithQualifier( aQualifier ); | ||
| 152 | |||
| 153 | if ( someFiles != null ) | ||
| 154 | { | ||
| 155 | someFiles = this.filteredArrayWithEntity( someFiles, anEntity ); | ||
| 156 | |||
| 157 | if ( someFiles != null ) | ||
| 158 | { | ||
| 159 | int count = someFiles.count(); | ||
| 160 | int counter = 0; | ||
| 161 | |||
| 162 | for ( int index = 0; index < count; index++ ) | ||
| 163 | { | ||
| 164 | File aFile = (File) someFiles.objectAtIndex( index ); | ||
| 165 | |||
| 166 | if ( aFile.delete() == true ) | ||
| 167 | { | ||
| 168 | counter += 1; | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | return counter; | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | return 0; | ||
| 177 | } | ||
| 178 | |||
| 179 | throw new IllegalArgumentException | ||
| 180 | ( "FSAdaptorChannel.deleteRowsDescribedByQualifier: null entity." ); | ||
| 181 | } | ||
| 182 | |||
| 183 | throw new IllegalArgumentException | ||
| 184 | ( "FSAdaptorChannel.deleteRowsDescribedByQualifier: null qualifier." ); | ||
| 185 | } | ||
| 186 | |||
| 187 | {{/code}} | ||
| 188 | |||
| 189 | (% class="alternate" %) | ||
| 190 | * insertRow will create a file or directory. | ||
| 191 | |||
| 192 | {{code}} | ||
| 193 | |||
| 194 | public void insertRow(NSDictionary aRow, EOEntity anEntity) | ||
| 195 | { | ||
| 196 | if ( aRow != null ) | ||
| 197 | { | ||
| 198 | if ( anEntity != null ) | ||
| 199 | { | ||
| 200 | String aPath = (String) aRow.objectForKey( "absolutePath" ); | ||
| 201 | |||
| 202 | if ( aPath != null ) | ||
| 203 | { | ||
| 204 | File aFile = new File( aPath ); | ||
| 205 | |||
| 206 | try | ||
| 207 | { | ||
| 208 | if ( anEntity.name().equals( "FSDirectory" ) == true ) | ||
| 209 | { | ||
| 210 | aFile.mkdirs(); | ||
| 211 | } | ||
| 212 | else | ||
| 213 | { | ||
| 214 | aFile.createNewFile(); | ||
| 215 | } | ||
| 216 | } | ||
| 217 | catch(Exception anException) | ||
| 218 | { | ||
| 219 | throw new RuntimeException | ||
| 220 | ( "FSAdaptorChannel.insertRow: " + anException ); | ||
| 221 | } | ||
| 222 | |||
| 223 | return; | ||
| 224 | } | ||
| 225 | |||
| 226 | throw new IllegalArgumentException | ||
| 227 | ( "FSAdaptorChannel.insertRow: null absolutePath." ); | ||
| 228 | } | ||
| 229 | |||
| 230 | throw new IllegalArgumentException | ||
| 231 | ( "FSAdaptorChannel.insertRow: null entity." ); | ||
| 232 | } | ||
| 233 | |||
| 234 | throw new IllegalArgumentException | ||
| 235 | ( "FSAdaptorChannel.insertRow: null row." ); | ||
| 236 | } | ||
| 237 | |||
| 238 | {{/code}} | ||
| 239 | |||
| 240 | (% class="alternate" %) | ||
| 241 | * updateValuesInRowsDescribedByQualifier will change a file attributes. | ||
| 242 | |||
| 243 | {{code}} | ||
| 244 | |||
| 245 | public int updateValuesInRowsDescribedByQualifier(NSDictionary | ||
| 246 | aRow, EOQualifier aQualifier, EOEntity anEntity) | ||
| 247 | { | ||
| 248 | if ( aRow != null ) | ||
| 249 | { | ||
| 250 | if ( aQualifier != null ) | ||
| 251 | { | ||
| 252 | if ( anEntity != null ) | ||
| 253 | { | ||
| 254 | NSArray someFiles = | ||
| 255 | FSQualifierHandler.filesWithQualifier( aQualifier ); | ||
| 256 | |||
| 257 | if ( someFiles != null ) | ||
| 258 | { | ||
| 259 | someFiles = this.filteredArrayWithEntity( someFiles, anEntity ); | ||
| 260 | |||
| 261 | if ( someFiles != null ) | ||
| 262 | { | ||
| 263 | int count = someFiles.count(); | ||
| 264 | |||
| 265 | for ( int index = 0; index < count; index++ ) | ||
| 266 | { | ||
| 267 | File aFile = (File) someFiles.objectAtIndex( index ); | ||
| 268 | NSArray someKeys = aRow.allKeys(); | ||
| 269 | int keyCount = someKeys.count(); | ||
| 270 | |||
| 271 | for ( int keyIndex = 0; keyIndex < count; keyIndex++ ) | ||
| 272 | { | ||
| 273 | Object aKey = someKeys.objectAtIndex( keyIndex ); | ||
| 274 | EOAttribute anAttribute = | ||
| 275 | anEntity.attributeNamed( aKey.toString() ); | ||
| 276 | |||
| 277 | if ( anAttribute != null ) | ||
| 278 | { | ||
| 279 | Object aValue = aRow.objectForKey( aKey ); | ||
| 280 | |||
| 281 | |||
| 282 | NSKeyValueCoding.DefaultImplementation.takeValueForKey( aFile, aValue, | ||
| 283 | anAttribute.columnName() ); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | |||
| 287 | } | ||
| 288 | |||
| 289 | return count; | ||
| 290 | } | ||
| 291 | } | ||
| 292 | |||
| 293 | return 0; | ||
| 294 | } | ||
| 295 | |||
| 296 | throw new IllegalArgumentException | ||
| 297 | ( "FSAdaptorChannel.updateValuesInRowsDescribedByQualifier: null | ||
| 298 | entity." ); | ||
| 299 | } | ||
| 300 | |||
| 301 | throw new IllegalArgumentException | ||
| 302 | ( "FSAdaptorChannel.updateValuesInRowsDescribedByQualifier: null | ||
| 303 | qualifier." ); | ||
| 304 | } | ||
| 305 | |||
| 306 | throw new IllegalArgumentException | ||
| 307 | ( "FSAdaptorChannel.updateValuesInRowsDescribedByQualifier: null row." ); | ||
| 308 | } | ||
| 309 | |||
| 310 | {{/code}} | ||
| 311 | |||
| 312 | (% class="alternate" %) | ||
| 313 | * selectAttributes will retrieve some files based on a qualifier. | ||
| 314 | |||
| 315 | {{code}} | ||
| 316 | |||
| 317 | public void selectAttributes(NSArray someAttributes, | ||
| 318 | EOFetchSpecification aFetchSpecification, boolean shouldLock, EOEntity | ||
| 319 | anEntity) | ||
| 320 | { | ||
| 321 | if ( someAttributes != null ) | ||
| 322 | { | ||
| 323 | this.setAttributesToFetch( someAttributes ); | ||
| 324 | |||
| 325 | if ( aFetchSpecification != null ) | ||
| 326 | { | ||
| 327 | if ( anEntity != null ) | ||
| 328 | { | ||
| 329 | NSArray someFiles = | ||
| 330 | FSQualifierHandler.filesWithQualifier( aFetchSpecification.qualifier() ); | ||
| 331 | |||
| 332 | if ( someFiles != null ) | ||
| 333 | { | ||
| 334 | NSArray someSortOrderings = aFetchSpecification.sortOrderings(); | ||
| 335 | |||
| 336 | if ( someSortOrderings != null ) | ||
| 337 | { | ||
| 338 | someFiles = | ||
| 339 | EOSortOrdering.sortedArrayUsingKeyOrderArray( someFiles, | ||
| 340 | someSortOrderings ); | ||
| 341 | } | ||
| 342 | |||
| 343 | someFiles = this.filteredArrayWithEntity( someFiles, anEntity ); | ||
| 344 | |||
| 345 | if ( someFiles != null ) | ||
| 346 | { | ||
| 347 | this.files().addObjectsFromArray( someFiles ); | ||
| 348 | } | ||
| 349 | } | ||
| 350 | |||
| 351 | return; | ||
| 352 | } | ||
| 353 | |||
| 354 | throw new IllegalArgumentException | ||
| 355 | ( "FSAdaptorChannel.selectAttributes: null entity." ); | ||
| 356 | } | ||
| 357 | |||
| 358 | throw new IllegalArgumentException | ||
| 359 | ( "FSAdaptorChannel.selectAttributes: null fetch specification." ); | ||
| 360 | } | ||
| 361 | |||
| 362 | throw new IllegalArgumentException | ||
| 363 | ( "FSAdaptorChannel.selectAttributes: null attributes." ); | ||
| 364 | } | ||
| 365 | |||
| 366 | {{/code}} | ||
| 367 | |||
| 368 | (% class="alternate" %) | ||
| 369 | * fetchRow will "fetch" a dictionary representation of a file. | ||
| 370 | |||
| 371 | {{code}} | ||
| 372 | |||
| 373 | public NSMutableDictionary fetchRow() | ||
| 374 | { | ||
| 375 | File aFile = (File) this.files().lastObject(); | ||
| 376 | |||
| 377 | if ( aFile != null ) | ||
| 378 | { | ||
| 379 | this.files().removeLastObject(); | ||
| 380 | |||
| 381 | return this.dictionaryForFileWithAttributes( aFile, | ||
| 382 | this.attributesToFetch() ); | ||
| 383 | } | ||
| 384 | |||
| 385 | return null; | ||
| 386 | } | ||
| 387 | |||
| 388 | {{/code}} | ||
| 389 | |||
| 390 | That could be it. Nothing to involved. However there is two extra | ||
| 391 | methods that make sense to implement: describeTableNames and | ||
| 392 | describeModelWithTableNames. | ||
| 393 | |||
| 394 | (% class="alternate" %) | ||
| 395 | * describeTableNames() return an array of "table" (or something | ||
| 396 | conceptually equivalent) existing in you data back end. | ||
| 397 | * describeModelWithTableNames() will create a default model for some of | ||
| 398 | these "tables". | ||
| 399 | |||
| 400 | This is very useful to get you started in EOModeler instead of having to | ||
| 401 | create the entire model by hand. In the case of a file system, there is | ||
| 402 | only one model: | ||
| 403 | |||
| 404 | {{code}} | ||
| 405 | |||
| 406 | public EOModel describeModelWithTableNames(NSArray anArray) | ||
| 407 | { | ||
| 408 | return new EOModel( this.defaultModelPath() ); | ||
| 409 | } | ||
| 410 | |||
| 411 | {{/code}} | ||
| 412 | |||
| 413 | And voilĂ . No you have a toy file system adaptor (aka JavaFSAdaptor). | ||
| 414 | |||
| 415 | (% class="alternate" %) | ||
| 416 | * To "fetch" some files, simply create a qualifier with some path | ||
| 417 | attributes (eg absolutePath): | ||
| 418 | |||
| 419 | {{code}} | ||
| 420 | |||
| 421 | EOQualifier aPathQualifier = new EOKeyValueQualifier | ||
| 422 | ( "absolutePath", EOQualifier.QualifierOperatorEqual, System.getProperty | ||
| 423 | ( "user.home" ) ); | ||
| 424 | |||
| 425 | {{/code}} | ||
| 426 | |||
| 427 | (% class="alternate" %) | ||
| 428 | * To get all the files in a directory, use the "parent" attribute: | ||
| 429 | |||
| 430 | {{code}} | ||
| 431 | |||
| 432 | EOQualifier aPathQualifier = new EOKeyValueQualifier( "parent", | ||
| 433 | EOQualifier.QualifierOperatorEqual, System.getProperty( "user.home" ) ); | ||
| 434 | |||
| 435 | {{/code}} | ||
| 436 | |||
| 437 | Once you have your qualifier, simply build a fetch specification for it | ||
| 438 | and get your "file" EOs: | ||
| 439 | |||
| 440 | {{code}} | ||
| 441 | |||
| 442 | EOFetchSpecification aFetchSpecification = new EOFetchSpecification | ||
| 443 | ( "FSDirectory", aQualifier, null ); | ||
| 444 | NSArray someObjects = | ||
| 445 | anEditingContext.objectsWithFetchSpecification( aFetchSpecification ); | ||
| 446 | |||
| 447 | {{/code}} | ||
| 448 | |||
| 449 | You can use FSFile to retrieve files only. FSDirectory for, er, | ||
| 450 | directories. And FSItem for both. FSDirectory have two handy | ||
| 451 | relationships: "files" and "directories". Check the attached model. | ||
| 452 | |||
| 453 | (% class="alternate" %) | ||
| 454 | * To "create" a file (or directory), you can do something along those | ||
| 455 | lines: | ||
| 456 | |||
| 457 | {{code}} | ||
| 458 | |||
| 459 | EOClassDescription aClassDescription = | ||
| 460 | EOClassDescription.classDescriptionForEntityName( "FSDirectory" ); | ||
| 461 | EOEnterpriseObject anObject = | ||
| 462 | aClassDescription.createInstanceWithEditingContext( anEditingContext, | ||
| 463 | null ); | ||
| 464 | |||
| 465 | anObject.takeValueForKey | ||
| 466 | ( "/Users/rszwarc/tmp/JavaFSAdaptor/TestFSDirectory", "absolutePath" ); | ||
| 467 | |||
| 468 | anEditingContext.insertObject( anObject ); | ||
| 469 | |||
| 470 | anEditingContext.saveChanges(); | ||
| 471 | |||
| 472 | {{/code}} | ||
| 473 | |||
| 474 | (% class="alternate" %) | ||
| 475 | * Finally, do delete a file: | ||
| 476 | |||
| 477 | {{code}} | ||
| 478 | |||
| 479 | anEditingContext.deleteObject( anObject ); | ||
| 480 | |||
| 481 | anEditingContext.saveChanges(); | ||
| 482 | |||
| 483 | {{/code}} |