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}} |